From f4739b554dcfe743d5036adbf872673882c0d5bb Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 17:29:18 +0200 Subject: [PATCH 1/7] Update Octicons to v19.35.0 Introduces op-flag icon. See opf/openproject-octicons#203 --- Gemfile | 4 ++-- Gemfile.lock | 14 +++++++------- frontend/package-lock.json | 14 +++++++------- frontend/package.json | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Gemfile b/Gemfile index a9d40ae75b88..f2b0e78dd5ee 100644 --- a/Gemfile +++ b/Gemfile @@ -427,6 +427,6 @@ gemfiles.each do |file| send(:eval_gemfile, file) if File.readable?(file) end -gem "openproject-octicons", "~>19.34.0" -gem "openproject-octicons_helper", "~>19.34.0" +gem "openproject-octicons", "~>19.35.0" +gem "openproject-octicons_helper", "~>19.35.0" gem "openproject-primer_view_components", "~>0.85.0" diff --git a/Gemfile.lock b/Gemfile.lock index 1c4a60cb34fa..58cb550a2e04 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -904,10 +904,10 @@ GEM validate_email validate_url webfinger (~> 2.0) - openproject-octicons (19.34.0) - openproject-octicons_helper (19.34.0) + openproject-octicons (19.35.0) + openproject-octicons_helper (19.35.0) actionview - openproject-octicons (= 19.34.0) + openproject-octicons (= 19.35.0) railties openproject-primer_view_components (0.85.0) actionview (>= 7.2.0) @@ -1684,8 +1684,8 @@ DEPENDENCIES openproject-job_status! openproject-ldap_groups! openproject-meeting! - openproject-octicons (~> 19.34.0) - openproject-octicons_helper (~> 19.34.0) + openproject-octicons (~> 19.35.0) + openproject-octicons_helper (~> 19.35.0) openproject-openid_connect! openproject-primer_view_components (~> 0.85.0) openproject-recaptcha! @@ -2065,8 +2065,8 @@ CHECKSUMS openproject-job_status (1.0.0) openproject-ldap_groups (1.0.0) openproject-meeting (1.0.0) - openproject-octicons (19.34.0) sha256=4efe8a58a2d8051b79c94b37e9a7f04fd242a4da12b50f027c3c7f441a042adc - openproject-octicons_helper (19.34.0) sha256=12eb7af2214e21631369c76464ebaa30de788e1074c4b3bd0fcef7e74cb9edb4 + openproject-octicons (19.35.0) sha256=a5033550d0961b4a8cb0993512a899716d633e17c2b5147bc6a9ed74f3952b38 + openproject-octicons_helper (19.35.0) sha256=c32d142a4bb7fda739b16768aa8846fd88ffc1750509d8056f516056e8767361 openproject-openid_connect (1.0.0) openproject-primer_view_components (0.85.0) sha256=16bc8358ef600f0465488a2e3c86991a9c69ed84580bd450c2dbec6f268eeaca openproject-recaptcha (1.0.0) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1cf4c73c8a5f..840c1228c2c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -56,7 +56,7 @@ "@ng-select/ng-option-highlight": "^21.8.2", "@ng-select/ng-select": "^21.8.0", "@ngneat/content-loader": "^7.0.0", - "@openproject/octicons-angular": "^19.34.0", + "@openproject/octicons-angular": "^19.35.0", "@openproject/primer-view-components": "^0.85.0", "@openproject/reactivestates": "^3.0.1", "@primer/css": "^22.1.0", @@ -7682,9 +7682,9 @@ "license": "BSD-3-Clause" }, "node_modules/@openproject/octicons-angular": { - "version": "19.34.0", - "resolved": "https://registry.npmjs.org/@openproject/octicons-angular/-/octicons-angular-19.34.0.tgz", - "integrity": "sha512-RsTK48htb8zwb1C4M3quhZG6uGFWYPICR2rO9jckCpww4MgWQZKfFrSCH8r43+uOczjYorwktzn7CIJywGW9Rg==", + "version": "19.35.0", + "resolved": "https://registry.npmjs.org/@openproject/octicons-angular/-/octicons-angular-19.35.0.tgz", + "integrity": "sha512-oN6bkZeOcrWUAJtfuXsMHmHWpuJMxIt1gvToJpsDgOXFY9Wj1DVO2Di/hMYgG/8k+xv2UZ3kAgks43ENImgLmw==", "dependencies": { "tslib": "^2.3.0" }, @@ -31149,9 +31149,9 @@ "integrity": "sha512-iFrvar5SOMtKFOSjYvs4z9UlLqDdJbMx0mgISLcPedv+g0ac5sgeETLGtipHCVIae6HJPclNEH5aCyD1RZaEHw==" }, "@openproject/octicons-angular": { - "version": "19.34.0", - "resolved": "https://registry.npmjs.org/@openproject/octicons-angular/-/octicons-angular-19.34.0.tgz", - "integrity": "sha512-RsTK48htb8zwb1C4M3quhZG6uGFWYPICR2rO9jckCpww4MgWQZKfFrSCH8r43+uOczjYorwktzn7CIJywGW9Rg==", + "version": "19.35.0", + "resolved": "https://registry.npmjs.org/@openproject/octicons-angular/-/octicons-angular-19.35.0.tgz", + "integrity": "sha512-oN6bkZeOcrWUAJtfuXsMHmHWpuJMxIt1gvToJpsDgOXFY9Wj1DVO2Di/hMYgG/8k+xv2UZ3kAgks43ENImgLmw==", "requires": { "tslib": "^2.3.0" } diff --git a/frontend/package.json b/frontend/package.json index fb87931e3c78..0fb47957c10d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -107,7 +107,7 @@ "@ng-select/ng-option-highlight": "^21.8.2", "@ng-select/ng-select": "^21.8.0", "@ngneat/content-loader": "^7.0.0", - "@openproject/octicons-angular": "^19.34.0", + "@openproject/octicons-angular": "^19.35.0", "@openproject/primer-view-components": "^0.85.0", "@openproject/reactivestates": "^3.0.1", "@primer/css": "^22.1.0", From 704a5f8795a036c62d55acda5620d3c78619b75e Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 18:09:35 +0200 Subject: [PATCH 2/7] [#72945] Update sprint button icons and labels Icons: :play -> :rocket, :check -> :op-flag. Labels: "Start" -> "Start sprint", "Complete" -> "Complete sprint". https://community.openproject.org/wp/72945 --- .../app/components/backlogs/sprint_component.html.erb | 4 ++-- modules/backlogs/config/locales/en.yml | 4 ++-- .../spec/components/backlogs/sprint_component_spec.rb | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/backlogs/app/components/backlogs/sprint_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_component.html.erb index c798db47e126..07de6c3cca75 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_component.html.erb @@ -63,7 +63,7 @@ See COPYRIGHT and LICENSE files for more details. <% if show_start_sprint_action? %> <% header.with_action_button(**start_sprint_button_arguments) do |button| %> - <% button.with_leading_visual_icon(icon: :play) %> + <% button.with_leading_visual_icon(icon: :rocket) %> <% if start_sprint_disabled_reason.present? %> <% button.with_tooltip( text: start_sprint_disabled_reason, @@ -75,7 +75,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% elsif show_finish_sprint_action? %> <% header.with_action_button(**finish_sprint_button_arguments) do |button| %> - <% button.with_leading_visual_icon(icon: :check) %> + <% button.with_leading_visual_icon(icon: :"op-flag") %> <%= t(".label_complete_sprint") %> <% end %> <% end %> diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index 15889180aa93..c4a568bcbc7b 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -142,8 +142,8 @@ en: blankslate_title: "%{name} is empty" blankslate_description: "No items planned yet. Drag items here to add them." label_actions: "Sprint actions" - label_start_sprint: "Start" - label_complete_sprint: "Complete" + label_start_sprint: "Start sprint" + label_complete_sprint: "Complete sprint" start_sprint_disabled_reason_active_sprint: "Another sprint is already active." start_sprint_disabled_reason_missing_dates: "Start and finish dates are required in order to start the sprint." action_menu: diff --git a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb index 1cfdce2d2439..c12fe76c9140 100644 --- a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb @@ -175,7 +175,7 @@ def menu_items end it "renders the start-sprint link enabled" do - expect(rendered_component).to have_link("Start") + expect(rendered_component).to have_link("Start sprint") end end @@ -187,7 +187,7 @@ def menu_items end it "renders the start-sprint button as disabled" do - expect(rendered_component).to have_selector(:link_or_button, "Start", aria: { disabled: true }) + expect(rendered_component).to have_selector(:link_or_button, "Start sprint", aria: { disabled: true }) end end @@ -200,7 +200,7 @@ def menu_items let!(:task_board) { create(:board_grid_with_query, project:, linked: sprint) } it "renders the complete-sprint link" do - expect(rendered_component).to have_link("Complete") + expect(rendered_component).to have_link("Complete sprint") end context "when params[:all] is true" do @@ -210,7 +210,7 @@ def menu_items it "preserves ?all=1 on the complete-sprint link" do expect(rendered_component).to have_link( - "Complete", + "Complete sprint", href: finish_project_backlogs_sprint_path(project, sprint, all: 1) ) end From d22bc0c0b8495e267ed2c40d294da95bafbbd6da Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 18:10:26 +0200 Subject: [PATCH 3/7] [#72945] Add sprint status badge to header Adds SprintStatusBadgeComponent with opaque bg: backgrounds following the Jira import StatusBadgeComponent pattern. https://community.openproject.org/wp/72945 --- .../backlogs/sprint_component.html.erb | 2 + .../sprint_status_badge_component.html.erb | 5 ++ .../backlogs/sprint_status_badge_component.rb | 52 +++++++++++++++++++ .../backlogs/sprint_component_spec.rb | 38 ++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 modules/backlogs/app/components/backlogs/sprint_status_badge_component.html.erb create mode 100644 modules/backlogs/app/components/backlogs/sprint_status_badge_component.rb diff --git a/modules/backlogs/app/components/backlogs/sprint_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_component.html.erb index 07de6c3cca75..634578309fda 100644 --- a/modules/backlogs/app/components/backlogs/sprint_component.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_component.html.erb @@ -44,6 +44,8 @@ See COPYRIGHT and LICENSE files for more details. ) do |list| %> <% list.with_header(title: sprint.name) do |header| %> <% header.with_description do %> + <%= render(Backlogs::SprintStatusBadgeComponent.new(sprint:, mr: 3)) %> + <%= render( Primer::Beta::Text.new( color: :subtle, mr: 3, classes: "velocity", diff --git a/modules/backlogs/app/components/backlogs/sprint_status_badge_component.html.erb b/modules/backlogs/app/components/backlogs/sprint_status_badge_component.html.erb new file mode 100644 index 000000000000..cfcb678e0060 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/sprint_status_badge_component.html.erb @@ -0,0 +1,5 @@ +<%= + render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, **@system_arguments)) do + I18n.t(:"activerecord.attributes.sprint.statuses.#{@sprint.status}") + end +%> diff --git a/modules/backlogs/app/components/backlogs/sprint_status_badge_component.rb b/modules/backlogs/app/components/backlogs/sprint_status_badge_component.rb new file mode 100644 index 000000000000..01be47a51dc8 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/sprint_status_badge_component.rb @@ -0,0 +1,52 @@ +# 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 SprintStatusBadgeComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + def initialize(sprint:, **system_arguments) + super() + + @sprint = sprint + @system_arguments = system_arguments.merge(bg: status_color) + end + + private + + def status_color + case @sprint.status + when "active" then :success + when "completed" then :done + else :default + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb index c12fe76c9140..0e0e4ea8a594 100644 --- a/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/sprint_component_spec.rb @@ -166,6 +166,44 @@ def menu_items end end + describe "sprint status badge in header description" do + context "when the sprint is in planning" do + let(:sprint) do + create(:sprint, project:, name: "Sprint 1", + start_date: Date.tomorrow, finish_date: Date.tomorrow + 7, + status: "in_planning") + end + + it "renders the status badge" do + expect(rendered_component).to have_css(".Label", text: "In planning") + end + end + + context "when the sprint is active" do + let(:sprint) do + create(:sprint, project:, name: "Sprint 1", + start_date: Date.yesterday, finish_date: Date.tomorrow, + status: "active") + end + + it "renders the status badge" do + expect(rendered_component).to have_css(".Label", text: "Active") + end + end + + context "when the sprint is completed" do + let(:sprint) do + create(:sprint, project:, name: "Sprint 1", + start_date: 2.weeks.ago, finish_date: 1.week.ago, + status: "completed") + end + + it "renders the status badge" do + expect(rendered_component).to have_css(".Label", text: "Completed") + end + end + end + describe "sprint actions in header" do context "when the sprint is in planning with date range set" do let(:sprint) do From e2428babf848ff9bc9908d1895c2d4ace150bd54 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 18:12:04 +0200 Subject: [PATCH 4/7] [#72945] Add inbox header with title and count https://community.openproject.org/wp/72945 --- .../backlogs/inbox_component.html.erb | 2 ++ modules/backlogs/config/locales/en.yml | 1 + .../backlogs/inbox_component_spec.rb | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/modules/backlogs/app/components/backlogs/inbox_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_component.html.erb index 169b533371c9..6a9aed34a57d 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_component.html.erb @@ -43,6 +43,8 @@ See COPYRIGHT and LICENSE files for more details. } ) ) do |list| %> + <% list.with_header(title: t(".title"), count: true) %> + <% list.with_empty_state( title: t(".blankslate_title"), description: t(".blankslate_description"), diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index c4a568bcbc7b..70a16549b788 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -151,6 +151,7 @@ en: add_work_package: "Add work package" inbox_component: + title: "Inbox" blankslate_title: "Backlog inbox is empty" blankslate_description: "All open work packages in this project will automatically appear here." show_more: diff --git a/modules/backlogs/spec/components/backlogs/inbox_component_spec.rb b/modules/backlogs/spec/components/backlogs/inbox_component_spec.rb index 0e362579e57d..4b835cc4a40e 100644 --- a/modules/backlogs/spec/components/backlogs/inbox_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/inbox_component_spec.rb @@ -71,6 +71,27 @@ def render_component end end + describe "header" do + let(:work_packages) do + [ + create(:work_package, subject: "First item", project:, story_points: 2, position: 1), + create(:work_package, subject: "Second item", project:, story_points: 4, position: 2) + ] + end + + it "renders the inbox title" do + expect(page).to have_heading "Inbox", level: 4 + end + + it "renders the work-package count" do + expect(page).to have_css( + ".Counter", + text: "2", + aria: { label: I18n.t(:label_x_items, count: 2) } + ) + end + end + describe "empty state" do let(:work_packages) { [] } From e9c4befa4d04c4fe138cb22172fa6ecf30f8f61d Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 20:34:38 +0200 Subject: [PATCH 5/7] [#72945] Add BorderBoxList collapse option Adds `collapsible:` (default `false`) to BorderBoxListComponent. When true, the header renders a CollapsibleHeader with toggle. When false, title/count/description render as plain components. https://community.openproject.org/wp/72945 --- .../common/border_box_list_component.rb | 12 +++++-- .../border_box_list_component/header.html.erb | 36 ++++++++++++------- .../border_box_list_component/header.rb | 13 ++++++- .../border_box_list_component_preview.rb | 23 ++++++++---- .../work_package_card_list_component.rb | 5 --- .../work_package_card_list_component_spec.rb | 26 -------------- .../common/border_box_list_component_spec.rb | 36 +++++++++++++++++-- 7 files changed, 94 insertions(+), 57 deletions(-) diff --git a/app/components/open_project/common/border_box_list_component.rb b/app/components/open_project/common/border_box_list_component.rb index 91fdced6ee3f..5cbe50d0c6f3 100644 --- a/app/components/open_project/common/border_box_list_component.rb +++ b/app/components/open_project/common/border_box_list_component.rb @@ -38,7 +38,9 @@ module Common class BorderBoxListComponent < ApplicationComponent include OpPrimer::ComponentHelpers - attr_reader :container, :current_user, :header_id, :footer_id + attr_reader :container, :collapsible, :current_user, :header_id, :footer_id + + alias_method :collapsible?, :collapsible # Optional header row. # @@ -53,6 +55,7 @@ class BorderBoxListComponent < ApplicationComponent renders_one :header, ->(**system_arguments) { system_arguments[:id] = header_id system_arguments[:list_id] = list_id + system_arguments[:collapsible] = collapsible? Header.new(**system_arguments) } @@ -152,13 +155,16 @@ class BorderBoxListComponent < ApplicationComponent # @param container [String, Symbol, Class, Object] value passed to # `dom_target` to derive DOM ids for the list and related controls. + # @param collapsible [Boolean] whether the header renders a collapsible + # toggle. Defaults to `false`. # @param current_user [User] user context passed to work-package items. # @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox`. # Pass `id:` to set the box id; related ids are derived from it. - def initialize(container:, current_user: User.current, **system_arguments) + def initialize(container:, collapsible: false, current_user: User.current, **system_arguments) super() @container = container + @collapsible = collapsible @current_user = current_user @system_arguments = system_arguments @@ -183,7 +189,7 @@ def configure_header! return unless header? header.resolve_count!(items.size) - return unless footer? + return unless collapsible? && footer? header.collapsible_id = [list_id, footer_id].compact.join(" ") end diff --git a/app/components/open_project/common/border_box_list_component/header.html.erb b/app/components/open_project/common/border_box_list_component/header.html.erb index b8a3fcd402a7..9f7d5f61cfb9 100644 --- a/app/components/open_project/common/border_box_list_component/header.html.erb +++ b/app/components/open_project/common/border_box_list_component/header.html.erb @@ -15,21 +15,33 @@ See COPYRIGHT and LICENSE files for more details. <%= grid_layout("op-border-box-list-header", tag: :div) do |grid| %> <% grid.with_area(:collapsible) do %> - <%= - render( - Primer::OpenProject::BorderBox::CollapsibleHeader.new( - collapsible_id:, - collapsed:, - multi_line: true - ) - ) do |collapsible| - %> - <% collapsible.with_title(tag: title_tag) { title } %> + <% if collapsible? %> + <%= + render( + Primer::OpenProject::BorderBox::CollapsibleHeader.new( + collapsible_id:, + collapsed:, + multi_line: true + ) + ) do |collapsible| + %> + <% collapsible.with_title(tag: title_tag) { title } %> + <% if render_count? %> + <% collapsible.with_count(**counter_arguments) %> + <% end %> + <% if description? %> + <% collapsible.with_description do %> + <%= description %> + <% end %> + <% end %> + <% end %> + <% else %> + <%= render(Primer::Beta::Truncate.new(tag: title_tag, classes: "Box-title")) { title } %> <% if render_count? %> - <% collapsible.with_count(**counter_arguments) %> + <%= render(Primer::Beta::Counter.new(**counter_arguments)) %> <% end %> <% if description? %> - <% collapsible.with_description do %> + <%= render(Primer::Beta::Text.new(color: :subtle, trim: true)) do %> <%= description %> <% end %> <% end %> diff --git a/app/components/open_project/common/border_box_list_component/header.rb b/app/components/open_project/common/border_box_list_component/header.rb index 8a6968d64f79..26d22f2f158d 100644 --- a/app/components/open_project/common/border_box_list_component/header.rb +++ b/app/components/open_project/common/border_box_list_component/header.rb @@ -80,7 +80,8 @@ class Header < ApplicationComponent :count_arguments, :title_tag, :list_id, - :collapsed + :collapsed, + :collapsible attr_writer :collapsible_id @@ -96,6 +97,9 @@ class Header < ApplicationComponent # @param title_tag [Symbol] tag used for the title heading. # @param list_id [String, nil] id of the collapsible list body. # @param collapsed [Boolean] whether the collapsible header starts closed. + # @param collapsible [Boolean] whether the header renders a collapsible + # toggle. Defaults to `true`. Pass `false` to render a plain title + # without a toggle button. # @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox#with_header`. def initialize( title:, @@ -105,6 +109,7 @@ def initialize( title_tag: :h4, list_id: nil, collapsed: false, + collapsible: true, **system_arguments ) super() @@ -117,9 +122,15 @@ def initialize( @list_id = list_id @collapsible_id = list_id @collapsed = collapsed + @collapsible = collapsible @system_arguments = system_arguments end + # @return [Boolean] whether a collapsible toggle should be rendered. + def collapsible? + collapsible + end + # Resolves inferred counts after the list slots have been captured. # # @param item_count [Integer] number of rendered item slots. diff --git a/lookbook/previews/open_project/common/border_box_list_component_preview.rb b/lookbook/previews/open_project/common/border_box_list_component_preview.rb index b8960fc4216d..6302bf5e57f9 100644 --- a/lookbook/previews/open_project/common/border_box_list_component_preview.rb +++ b/lookbook/previews/open_project/common/border_box_list_component_preview.rb @@ -33,9 +33,11 @@ module Common # @logical_path OpenProject/Common class BorderBoxListComponentPreview < ViewComponent::Preview # @label Default - def default + # @param collapsible [Boolean] toggle + def default(collapsible: false) render OpenProject::Common::BorderBoxListComponent.new( - container: "border-box-list-preview" + container: "border-box-list-preview", + collapsible: ) do |list| list.with_header(title: "Things we're building", count: true) do |header| header.with_description { "There's lots to look forward to" } @@ -58,12 +60,14 @@ def default end # @label With work package items - def with_work_package_items + # @param collapsible [Boolean] toggle + def with_work_package_items(collapsible: false) work_packages = WorkPackage.includes(:project).limit(2).to_a return preview_message("No work packages in the database.") if work_packages.empty? render OpenProject::Common::BorderBoxListComponent.new( - container: "border-box-list-work-package-preview" + container: "border-box-list-work-package-preview", + collapsible: ) do |list| list.with_header(title: "Work packages", count: true) @@ -80,18 +84,21 @@ def with_work_package_items end # @label Playground + # @param collapsible [Boolean] toggle # @param title_tag [Symbol] select [h2, h3, h4, h5] # @param count [Symbol] select [inferred, hidden, explicit, zero] # @param count_scheme [Symbol] select [primary, secondary] # @param hide_zero_count toggle def playground( + collapsible: false, title_tag: :h4, count: :inferred, count_scheme: :primary, hide_zero_count: true ) render OpenProject::Common::BorderBoxListComponent.new( - container: "border-box-list-playground-preview" + container: "border-box-list-playground-preview", + collapsible: ) do |list| list.with_header( title: "Playground list", @@ -114,9 +121,11 @@ def playground( # @label Empty state # List with a header and an empty state (Blankslate), no items. - def empty_state + # @param collapsible [Boolean] toggle + def empty_state(collapsible: false) render OpenProject::Common::BorderBoxListComponent.new( - container: "border-box-list-empty-preview" + container: "border-box-list-empty-preview", + collapsible: ) do |list| list.with_header(title: "Empty list", count: 0) list.with_empty_state( diff --git a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb index c5f5edc570d9..d9b1bdb97fae 100644 --- a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb +++ b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb @@ -78,7 +78,6 @@ def with_header(title:, **system_arguments, &) title:, count:, count_label: I18n.t(:label_x_work_packages, count:), - collapsed: folded?, **system_arguments, & ) @@ -114,10 +113,6 @@ def drag_and_drop_data } end - def folded? - current_user.pref[:backlogs_versions_default_fold_state] == "closed" - end - def populate_list! return if work_packages.empty? diff --git a/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb b/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb index 28a882cfa0b0..3f4ea4d4d0bf 100644 --- a/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb +++ b/modules/backlogs/spec/components/backlogs/work_package_card_list_component_spec.rb @@ -143,32 +143,6 @@ def render_component(work_packages:, container:, drag_and_drop:) expect(rendered_component).to have_heading "Sprint 1", level: 4 end - context "when the user prefers closed folds" do - before do - user.pref[:backlogs_versions_default_fold_state] = "closed" - end - - it "renders the header as collapsed" do - expect(rendered_component).to have_css( - ".CollapsibleHeader-triggerArea", - aria: { expanded: "false" } - ) - end - end - - context "when the user prefers open folds" do - before do - user.pref[:backlogs_versions_default_fold_state] = "open" - end - - it "renders the header as expanded" do - expect(rendered_component).to have_css( - ".CollapsibleHeader-triggerArea", - aria: { expanded: "true" } - ) - end - end - context "with work packages" do let(:header_arguments) { { title: "Sprint 1" } } let(:work_packages) do diff --git a/spec/components/open_project/common/border_box_list_component_spec.rb b/spec/components/open_project/common/border_box_list_component_spec.rb index 5d0b3deec4af..5c997141ac9f 100644 --- a/spec/components/open_project/common/border_box_list_component_spec.rb +++ b/spec/components/open_project/common/border_box_list_component_spec.rb @@ -358,7 +358,7 @@ def call describe "header collapsible behavior" do it "sets collapsible_id from list and footer ids" do rendered = render_inline( - described_class.new(container: "collapse-test") + described_class.new(container: "collapse-test", collapsible: true) ) do |list| list.with_header(title: "Collapsible") list.with_item { "row" } @@ -375,7 +375,7 @@ def call it "sets collapsible_id from list id only when no footer" do rendered = render_inline( - described_class.new(container: "collapse-no-footer") + described_class.new(container: "collapse-no-footer", collapsible: true) ) do |list| list.with_header(title: "No footer") list.with_item { "row" } @@ -620,7 +620,7 @@ def call it "derives the footer id from the explicit box id" do rendered = render_inline( - described_class.new(container: "ignored", id: "explicit-box") + described_class.new(container: "ignored", id: "explicit-box", collapsible: true) ) do |list| list.with_header(title: "Header") list.with_item { "row" } @@ -660,4 +660,34 @@ def call expect { described_class.new }.to raise_error(ArgumentError) end end + + describe "collapsible" do + it "renders a non-collapsible header by default" do + rendered = render_inline( + described_class.new(container: "no-collapse") + ) do |list| + list.with_header(title: "Non-collapsible header", count: 3) do |header| + header.with_description { "Description text" } + end + list.with_item { "row" } + end + + expect(rendered).to have_heading("Non-collapsible header", level: 4) + expect(rendered).to have_css(".Counter", text: "3") + expect(rendered).to have_text("Description text") + expect(rendered).to have_no_css("collapsible-header") + expect(rendered).to have_no_css("[aria-controls]") + end + + it "renders a collapsible header when collapsible is true" do + rendered = render_inline( + described_class.new(container: "explicit-collapse", collapsible: true) + ) do |list| + list.with_header(title: "Collapsible header") + list.with_item { "row" } + end + + expect(rendered).to have_css("collapsible-header") + end + end end From 496343d2695a8f05a3a55b4befd06bebe24d3c17 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 20:35:40 +0200 Subject: [PATCH 6/7] [#72945] Add BorderBoxList flat scheme Adds `scheme: :flat` to BorderBoxListComponent with CSS overrides for transparent header background and no separator line. Backlogs callers adopt the flat scheme. https://community.openproject.org/wp/72945 --- .../common/border_box_list_component.rb | 24 +++++++++++-- .../common/border_box_list_component.sass | 8 +++++ .../border_box_list_component_preview.rb | 27 ++++++++++++++ .../backlogs/inbox_component.html.erb | 1 + .../work_package_card_list_component.rb | 1 + .../common/border_box_list_component_spec.rb | 36 +++++++++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/components/open_project/common/border_box_list_component.rb b/app/components/open_project/common/border_box_list_component.rb index 5cbe50d0c6f3..0bfedd946099 100644 --- a/app/components/open_project/common/border_box_list_component.rb +++ b/app/components/open_project/common/border_box_list_component.rb @@ -37,8 +37,12 @@ module Common # header actions, collapsible behavior, and row rendering. class BorderBoxListComponent < ApplicationComponent include OpPrimer::ComponentHelpers + include Primer::FetchOrFallbackHelper - attr_reader :container, :collapsible, :current_user, :header_id, :footer_id + SCHEME_DEFAULT = :default + SCHEME_OPTIONS = [SCHEME_DEFAULT, :flat].freeze + + attr_reader :container, :scheme, :collapsible, :current_user, :header_id, :footer_id alias_method :collapsible?, :collapsible @@ -155,21 +159,37 @@ class BorderBoxListComponent < ApplicationComponent # @param container [String, Symbol, Class, Object] value passed to # `dom_target` to derive DOM ids for the list and related controls. + # @param scheme [Symbol] visual scheme. `:default` renders the standard + # BorderBox header. `:flat` renders a transparent header with no + # separator line. # @param collapsible [Boolean] whether the header renders a collapsible # toggle. Defaults to `false`. # @param current_user [User] user context passed to work-package items. # @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox`. # Pass `id:` to set the box id; related ids are derived from it. - def initialize(container:, collapsible: false, current_user: User.current, **system_arguments) + def initialize( # rubocop:disable Metrics/AbcSize + container:, + scheme: SCHEME_DEFAULT, + collapsible: false, + current_user: User.current, + **system_arguments + ) super() @container = container + @scheme = ActiveSupport::StringInquirer.new(fetch_or_fallback(SCHEME_OPTIONS, scheme, SCHEME_DEFAULT).to_s) @collapsible = collapsible @current_user = current_user @system_arguments = system_arguments @system_arguments[:id] ||= dom_target(container) @system_arguments[:list_id] = dom_target(@system_arguments[:id], :list) + @system_arguments[:classes] = class_names( + @system_arguments[:classes], + "BorderBoxList", + "BorderBoxList--flat" => @scheme.flat? + ) + @header_id = dom_target(@system_arguments[:id], :header) @footer_id = dom_target(@system_arguments[:id], :footer) end diff --git a/app/components/open_project/common/border_box_list_component.sass b/app/components/open_project/common/border_box_list_component.sass index 11a8520a0327..ce559825a242 100644 --- a/app/components/open_project/common/border_box_list_component.sass +++ b/app/components/open_project/common/border_box_list_component.sass @@ -20,3 +20,11 @@ align-self: flex-start // Unfortunately, the invisible button style bites us here again. margin-top: -6px + +.BorderBoxList--flat + > .Box-header + background-color: transparent + border-bottom: none + + > ul > .Box-row:first-of-type + border-top: none diff --git a/lookbook/previews/open_project/common/border_box_list_component_preview.rb b/lookbook/previews/open_project/common/border_box_list_component_preview.rb index 6302bf5e57f9..a89e8981c0e6 100644 --- a/lookbook/previews/open_project/common/border_box_list_component_preview.rb +++ b/lookbook/previews/open_project/common/border_box_list_component_preview.rb @@ -59,6 +59,33 @@ def default(collapsible: false) end end + # @label Flat scheme + # @param collapsible [Boolean] toggle + def flat(collapsible: false) + render OpenProject::Common::BorderBoxListComponent.new( + container: "border-box-list-flat-preview", + scheme: :flat, + collapsible: + ) do |list| + list.with_header(title: "Sprint backlog", count: true) do |header| + header.with_description { "3 points remaining" } + header.with_action_button do |button| + button.with_leading_visual_icon(icon: :rocket) + "Start sprint" + end + header.with_menu(button_aria_label: "Sprint actions") do |menu| + menu.with_item(label: "Edit sprint") do |menu_item| + menu_item.with_leading_visual_icon(icon: :pencil) + end + end + end + + list.with_item { "User authentication stories" } + list.with_item { "Dashboard improvements" } + list.with_item { "API documentation" } + end + end + # @label With work package items # @param collapsible [Boolean] toggle def with_work_package_items(collapsible: false) diff --git a/modules/backlogs/app/components/backlogs/inbox_component.html.erb b/modules/backlogs/app/components/backlogs/inbox_component.html.erb index 6a9aed34a57d..3290af8e1171 100644 --- a/modules/backlogs/app/components/backlogs/inbox_component.html.erb +++ b/modules/backlogs/app/components/backlogs/inbox_component.html.erb @@ -33,6 +33,7 @@ See COPYRIGHT and LICENSE files for more details. OpenProject::Common::BorderBoxListComponent.new( container: inbox_container, current_user:, + scheme: :flat, padding: :condensed, test_selector: "backlog-inbox", data: { diff --git a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb index d9b1bdb97fae..40b18c10a7a6 100644 --- a/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb +++ b/modules/backlogs/app/components/backlogs/work_package_card_list_component.rb @@ -67,6 +67,7 @@ def initialize( @list = OpenProject::Common::BorderBoxListComponent.new( container:, current_user:, + scheme: :flat, **@system_arguments ) end diff --git a/spec/components/open_project/common/border_box_list_component_spec.rb b/spec/components/open_project/common/border_box_list_component_spec.rb index 5c997141ac9f..735038f7d7f6 100644 --- a/spec/components/open_project/common/border_box_list_component_spec.rb +++ b/spec/components/open_project/common/border_box_list_component_spec.rb @@ -690,4 +690,40 @@ def call expect(rendered).to have_css("collapsible-header") end end + + describe "scheme" do + it "defaults to :default" do + rendered = render_inline( + described_class.new(container: "scheme-default") + ) do |list| + list.with_header(title: "Default") + list.with_item { "row" } + end + + expect(rendered).to have_no_css(".BorderBoxList--flat") + end + + it "applies the flat CSS class when scheme is :flat" do + rendered = render_inline( + described_class.new(container: "scheme-flat", scheme: :flat) + ) do |list| + list.with_header(title: "Flat") + list.with_item { "row" } + end + + expect(rendered).to have_css(".Box.BorderBoxList--flat") + end + + it "keeps collapsible independent of the flat scheme" do + rendered = render_inline( + described_class.new(container: "flat-collapse", scheme: :flat, collapsible: true) + ) do |list| + list.with_header(title: "Flat collapsible") + list.with_item { "row" } + end + + expect(rendered).to have_css(".Box.BorderBoxList--flat") + expect(rendered).to have_css("collapsible-header") + end + end end From d7690a35179573613cf3d3128948ebc7c27cae0c Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Thu, 14 May 2026 20:45:19 +0200 Subject: [PATCH 7/7] [#72945] Align non-collapsible header layout Restructures the non-collapsible header branch to mirror CollapsibleHeader's HTML structure: title+count on one row, description on a second row. Reuses upstream CSS classes. https://community.openproject.org/wp/72945 --- .../border_box_list_component/header.html.erb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/components/open_project/common/border_box_list_component/header.html.erb b/app/components/open_project/common/border_box_list_component/header.html.erb index 9f7d5f61cfb9..9bc37cafcc09 100644 --- a/app/components/open_project/common/border_box_list_component/header.html.erb +++ b/app/components/open_project/common/border_box_list_component/header.html.erb @@ -36,13 +36,17 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% end %> <% else %> - <%= render(Primer::Beta::Truncate.new(tag: title_tag, classes: "Box-title")) { title } %> - <% if render_count? %> - <%= render(Primer::Beta::Counter.new(**counter_arguments)) %> - <% end %> - <% if description? %> - <%= render(Primer::Beta::Text.new(color: :subtle, trim: true)) do %> - <%= description %> + <%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader CollapsibleHeader--multi-line")) do %> + <%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader-title-line")) do %> + <%= render(Primer::Beta::Truncate.new(tag: title_tag, classes: "CollapsibleHeader-title Box-title")) { title } %> + <% if render_count? %> + <%= render(Primer::Beta::Counter.new(**counter_arguments, classes: class_names(counter_arguments[:classes], "CollapsibleHeader-count"))) %> + <% end %> + <% end %> + <% if description? %> + <%= render(Primer::Beta::Text.new(color: :subtle, trim: true, classes: "CollapsibleHeader-description")) do %> + <%= description %> + <% end %> <% end %> <% end %> <% end %>