From b47ce31523206ba53ddbd12db803dc2c782e4feb Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 4 Dec 2024 14:32:40 +0100 Subject: [PATCH 01/27] [#58160] project list: query for life cycle steps This enables us to select a life cycle step as a view. It breaks as soon as you try to render the table including this field type, though. --- app/models/queries/projects.rb | 1 + .../projects/selects/life_cycle_step.rb | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 app/models/queries/projects/selects/life_cycle_step.rb diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index bb25098c5322..64b721b7596a 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -62,6 +62,7 @@ module Queries::Projects select Selects::CustomField select Selects::Default select Selects::LatestActivityAt + select Selects::LifeCycleStep select Selects::RequiredDiskSpace select Selects::Status select Selects::Favored diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb new file mode 100644 index 000000000000..38086095bab5 --- /dev/null +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -0,0 +1,62 @@ +# 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. +# ++ + +class Queries::Projects::Selects::LifeCycleStep < Queries::Selects::Base + KEY = /\Alife_cycle_step_(\d+)\z/ + + def self.key + KEY + end + + def self.all_available + return [] unless available? + + Project::LifeCycleStep + .where(active: true) + .pluck(:id) + .map { |id| new(:"life_cycle_step_#{id}") } + end + + def caption + life_cycle_step.definition.name + end + + def life_cycle_step + return @life_cycle_step if defined?(@life_cycle_step) + + @life_cycle_step = Project::LifeCycleStep + .where(active: true) + .find_by(id: attribute[KEY, 1]) + end + + def available? + life_cycle_step.present? + end +end From 3796f8f33a5af199551ccc83ab55cfa00248e298 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Thu, 5 Dec 2024 10:48:24 +0100 Subject: [PATCH 02/27] [#58160] project list: show life cycle name in column for now --- app/components/projects/row_component.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 3f540eac0ecd..cc8ad40c0da0 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -69,6 +69,8 @@ def currently_favored? def column_value(column) if custom_field_column?(column) custom_field_column(column) + elsif life_cycle_step_column?(column) + life_cycle_step_column(column) else send(column.attribute) end @@ -94,6 +96,15 @@ def custom_field_column(column) end end + def life_cycle_step_column(column) + return nil unless user_can_view_project? + + # FIXME: make this efficient + ls = Project::LifeCycleStep.find_by(id: column.life_cycle_step.id, project:) + # FIXME: we are interested in the start/end date to display values here instead of the name + ls&.definition&.name + end + def created_at helpers.format_date(project.created_at) end @@ -374,6 +385,10 @@ def custom_field_column?(column) column.is_a?(::Queries::Projects::Selects::CustomField) end + def life_cycle_step_column?(column) + column.is_a?(::Queries::Projects::Selects::LifeCycleStep) + end + def current_page table.model.current_page.to_s end From a22cf911250b947cbb5b405b1fa11683cab07faa Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Thu, 5 Dec 2024 15:20:58 +0100 Subject: [PATCH 03/27] [#58160] show lifecycle dates in row --- app/components/projects/row_component.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index cc8ad40c0da0..21dfb448cab6 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -101,8 +101,14 @@ def life_cycle_step_column(column) # FIXME: make this efficient ls = Project::LifeCycleStep.find_by(id: column.life_cycle_step.id, project:) - # FIXME: we are interested in the start/end date to display values here instead of the name - ls&.definition&.name + + return nil if ls.blank? + + if ls.end_date + "#{helpers.format_date(ls.start_date)} - #{helpers.format_date(ls.end_date)}" + else + helpers.format_date ls.start_date.to_s + end end def created_at From f64d6e78ffb443075cf757af5f2cb040f6180ec2 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Thu, 5 Dec 2024 16:18:36 +0100 Subject: [PATCH 04/27] [#58160] use formatting helper --- app/components/projects/row_component.rb | 28 ++++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 21dfb448cab6..dc5a6bf81599 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -104,19 +104,15 @@ def life_cycle_step_column(column) return nil if ls.blank? - if ls.end_date - "#{helpers.format_date(ls.start_date)} - #{helpers.format_date(ls.end_date)}" - else - helpers.format_date ls.start_date.to_s - end + format_date(ls.start_date, ls.end_date) end def created_at - helpers.format_date(project.created_at) + format_date(project.created_at) end def latest_activity_at - helpers.format_date(project.latest_activity_at) + format_date(project.latest_activity_at) end def required_disk_space @@ -398,5 +394,23 @@ def life_cycle_step_column?(column) def current_page table.model.current_page.to_s end + + private + + # If only the `start_date` is given, will return a formatted version of that date as string. + # When `end_date` is given as well, will return a representation of the date range from start to end. + # @example + # format_date(Date.new(2024, 12, 4)) + # "04/12/2024" + # + # format_date(Date.new(2024, 12, 4), Date.new(2024, 12, 10)) + # "04/12/2024 - 10/12/2024" + def format_date(start_date, end_date = nil) + if end_date.present? + "#{helpers.format_date(start_date)} - #{helpers.format_date(end_date)}" + else + helpers.format_date(start_date) + end + end end end From 47ec2bfcdab3fca9c3d3c0b6016eb7e5cddcc192 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 6 Dec 2024 14:29:24 +0100 Subject: [PATCH 05/27] [#58160] refactor helper --- app/components/projects/row_component.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index dc5a6bf81599..c978b66a2fc2 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -104,15 +104,15 @@ def life_cycle_step_column(column) return nil if ls.blank? - format_date(ls.start_date, ls.end_date) + fmt_date_or_range(ls.start_date, ls.end_date) end def created_at - format_date(project.created_at) + helpers.format_date(project.created_at) end def latest_activity_at - format_date(project.latest_activity_at) + helpers.format_date(project.latest_activity_at) end def required_disk_space @@ -400,17 +400,16 @@ def current_page # If only the `start_date` is given, will return a formatted version of that date as string. # When `end_date` is given as well, will return a representation of the date range from start to end. # @example - # format_date(Date.new(2024, 12, 4)) + # fmt_date_or_range(Date.new(2024, 12, 4)) # "04/12/2024" # - # format_date(Date.new(2024, 12, 4), Date.new(2024, 12, 10)) + # fmt_date_or_range(Date.new(2024, 12, 4), Date.new(2024, 12, 10)) # "04/12/2024 - 10/12/2024" - def format_date(start_date, end_date = nil) - if end_date.present? - "#{helpers.format_date(start_date)} - #{helpers.format_date(end_date)}" - else - helpers.format_date(start_date) - end + def fmt_date_or_range(start_date, end_date = nil) + [start_date, end_date] + .compact + .map { |d| helpers.format_date(d) } + .join(" - ") end end end From 5e9fa54b7bdf5ceabd85d2cf0e5dc1cdb38b0a2e Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 09:20:03 +0100 Subject: [PATCH 06/27] [#58160] show correct step for project in table --- app/components/projects/row_component.rb | 4 +++- .../queries/projects/selects/life_cycle_step.rb | 12 +++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index c978b66a2fc2..4bae6784d01c 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -100,7 +100,9 @@ def life_cycle_step_column(column) return nil unless user_can_view_project? # FIXME: make this efficient - ls = Project::LifeCycleStep.find_by(id: column.life_cycle_step.id, project:) + ls = Project::LifeCycleStep + .where(active: true) + .find_by(definition_id: column.life_cycle_step.id, project:) return nil if ls.blank? diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 38086095bab5..55725c053452 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -29,7 +29,7 @@ # ++ class Queries::Projects::Selects::LifeCycleStep < Queries::Selects::Base - KEY = /\Alife_cycle_step_(\d+)\z/ + KEY = /\Alcsd_(\d+)\z/ def self.key KEY @@ -38,21 +38,19 @@ def self.key def self.all_available return [] unless available? - Project::LifeCycleStep - .where(active: true) + Project::LifeCycleStepDefinition .pluck(:id) - .map { |id| new(:"life_cycle_step_#{id}") } + .map { |id| new(:"lcsd_#{id}") } end def caption - life_cycle_step.definition.name + life_cycle_step.name end def life_cycle_step return @life_cycle_step if defined?(@life_cycle_step) - @life_cycle_step = Project::LifeCycleStep - .where(active: true) + @life_cycle_step = Project::LifeCycleStepDefinition .find_by(id: attribute[KEY, 1]) end From 36de7807a6f981feba50ce29e166f5bec4050b2d Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 09:30:08 +0100 Subject: [PATCH 07/27] [#58160] disable filter column for life cycle steps This is not pretty, but will be removed once #59183 is implemented. --- app/helpers/sort_helper.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index 3ec0eadd99e7..e0a557d56b15 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -381,6 +381,9 @@ def build_columns_link(columns, allowed_params: nil, **html_options) def find_filter_for_column(column, filter_mapping) col = column.to_s + # Temporarily disabled filters for stages and gates columns for now. Remove this line for #59183 + return nil if column.start_with?("lcsd_") + filter_mapping.fetch(col, col) end From 69047d5993bf6b78265e386a01facb8822acbf32 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 12:09:38 +0100 Subject: [PATCH 08/27] [#58160] use filter_map instead Thanks, Rubocop! --- app/helpers/projects_helper.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c01a2c1bc15c..2a733586906b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -73,8 +73,7 @@ def projects_columns_options def selected_projects_columns_options Setting .enabled_projects_columns - .map { |c| projects_columns_options.find { |o| o[:id].to_s == c } } - .compact + .filter_map { |c| projects_columns_options.find { |o| o[:id].to_s == c } } end def protected_projects_columns_options From 3ccf62532e77360475371f6b2620d9ffdeed8b4b Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 13:38:09 +0100 Subject: [PATCH 09/27] [#58160] show icons in life cycle list headers --- .../projects/table_component.html.erb | 2 +- app/helpers/sort_helper.rb | 26 +++++++++++-------- .../projects/selects/life_cycle_step.rb | 8 ++++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/components/projects/table_component.html.erb b/app/components/projects/table_component.html.erb index a114faeff841..9f33feb4aeea 100644 --- a/app/components/projects/table_component.html.erb +++ b/app/components/projects/table_component.html.erb @@ -56,7 +56,7 @@ See COPYRIGHT and LICENSE files for more details. <% else %> <% if use_quick_action_table_headers? %> - <%= quick_action_table_header column.attribute, order_options(column, turbo: true) %> + <%= quick_action_table_header column, order_options(column, turbo: true) %> <% else %>
diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index e0a557d56b15..daf2c395ae0f 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -331,8 +331,8 @@ def sort_header_tag(column, allowed_params: nil, **options) # This is a more specific version of #sort_header_tag. # For "filter by" to work properly, you must pass a Hash for `filter_column_mapping`. def sort_header_with_action_menu(column, all_columns, filter_column_mapping = {}, allowed_params: nil, **options) - with_sort_header_options(column, allowed_params:, **options) do |col, cap, default_order, **opts| - action_menu(col, all_columns, cap, default_order, filter_column_mapping, **opts) + with_sort_header_options(column.attribute, allowed_params:, **options) do |_col, cap, default_order, **opts| + action_menu(column, all_columns, cap, default_order, filter_column_mapping, **opts) end end @@ -392,9 +392,11 @@ def find_filter_for_column(column, filter_mapping) # Some of the method arguments are only needed for specific actions. def action_menu(column, table_columns, caption, default_order, filter_column_mapping = {}, allowed_params: nil, **html_options) - caption ||= column.to_s.humanize - filter = find_filter_for_column(column, filter_column_mapping) + attribute = column.attribute + caption ||= attribute.to_s.humanize + + filter = find_filter_for_column(attribute, filter_column_mapping) sortable = html_options.delete(:sortable) # `param` is not needed in the `content_arguments`, but should remain in the `html_options`. @@ -402,25 +404,27 @@ def action_menu(column, table_columns, caption, default_order, filter_column_map # the action menu. content_args = html_options.merge(rel: :nofollow, param: nil) - render Primer::Alpha::ActionMenu.new(menu_id: "menu-#{column}") do |menu| - action_button(menu, caption, favorite: column == :favored) + render Primer::Alpha::ActionMenu.new(menu_id: "menu-#{attribute}") do |menu| + action_button(menu, column, caption, favorite: column == :favored) # Some columns are not sortable or do not offer a suitable filter. Omit those actions for them. - sort_actions(menu, column, default_order, content_args:, allowed_params:, **html_options) if sortable - filter_action(menu, column, filter, content_args:) if filter + sort_actions(menu, attribute, default_order, content_args:, allowed_params:, **html_options) if sortable + filter_action(menu, attribute, filter, content_args:) if filter - move_column_actions(menu, column, table_columns, content_args:, allowed_params:, **html_options) - add_and_remove_column_actions(menu, column, table_columns, content_args:, allowed_params:, **html_options) + move_column_actions(menu, attribute, table_columns, content_args:, allowed_params:, **html_options) + add_and_remove_column_actions(menu, attribute, table_columns, content_args:, allowed_params:, **html_options) end end - def action_button(menu, caption, favorite: false) + def action_button(menu, column, caption, favorite: false) menu.with_show_button(scheme: :link, color: :default, text_transform: :uppercase, underline: false, display: :inline_flex, classes: "generic-table--action-menu-button") do |button| if favorite # This column only shows an icon, no text. render Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, "aria-label": I18n.t(:label_favorite)) + elsif column.respond_to?(:action_menu_header) + column.action_menu_header(button) else button.with_trailing_action_icon(icon: :"triangle-down") diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 55725c053452..6143283809bd 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -57,4 +57,12 @@ def life_cycle_step def available? life_cycle_step.present? end + + def action_menu_header(button) + icon = life_cycle_step.is_a?(Project::StageDefinition) ? :"git-commit" : :diamond + button.with_leading_visual_icon(icon:) + button.with_trailing_action_icon(icon: :"triangle-down") + + caption.to_s + end end From bf6ef1a9f013c8396a5d5d80913f2b9f34fcab69 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 15:56:30 +0100 Subject: [PATCH 10/27] [#58160] apply the correct color to life cycle header icons --- .../queries/projects/selects/life_cycle_step.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 6143283809bd..3f9a405cbbec 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -59,10 +59,19 @@ def available? end def action_menu_header(button) + # Show the proper icon for the definition with the correct color. icon = life_cycle_step.is_a?(Project::StageDefinition) ? :"git-commit" : :diamond - button.with_leading_visual_icon(icon:) + button.with_leading_visual_icon(icon:, classes: helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step)) + + # As all other action menu headers, we will show an action icon and the caption: button.with_trailing_action_icon(icon: :"triangle-down") caption.to_s end + + private + + def helpers + @helpers ||= Object.new.extend(ColorsHelper) + end end From 6a51ac464152ad192032d56daef9701b0b6c588d Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 9 Dec 2024 17:56:09 +0100 Subject: [PATCH 11/27] [#58160] fix visual glitch with leading icon in action button --- app/helpers/sort_helper.rb | 7 ++++++- app/models/queries/projects/selects/life_cycle_step.rb | 7 ++++++- frontend/src/global_styles/content/_table.sass | 6 ++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index daf2c395ae0f..844c9a5b5679 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -417,9 +417,14 @@ def action_menu(column, table_columns, caption, default_order, filter_column_map end def action_button(menu, column, caption, favorite: false) + additional_menu_classes = ["generic-table--action-menu-button", + column.respond_to?(:action_menu_classes) ? column.action_menu_classes : nil] + .compact + .join(" ") + menu.with_show_button(scheme: :link, color: :default, text_transform: :uppercase, underline: false, display: :inline_flex, - classes: "generic-table--action-menu-button") do |button| + classes: additional_menu_classes) do |button| if favorite # This column only shows an icon, no text. render Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, "aria-label": I18n.t(:label_favorite)) diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 3f9a405cbbec..a756695d4f46 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -61,7 +61,8 @@ def available? def action_menu_header(button) # Show the proper icon for the definition with the correct color. icon = life_cycle_step.is_a?(Project::StageDefinition) ? :"git-commit" : :diamond - button.with_leading_visual_icon(icon:, classes: helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step)) + classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step) + button.with_leading_visual_icon(icon:, classes:) # As all other action menu headers, we will show an action icon and the caption: button.with_trailing_action_icon(icon: :"triangle-down") @@ -69,6 +70,10 @@ def action_menu_header(button) caption.to_s end + def action_menu_classes + "leading-visual-icon-header" + end + private def helpers diff --git a/frontend/src/global_styles/content/_table.sass b/frontend/src/global_styles/content/_table.sass index aab02d6834d1..c3699bdbb48b 100644 --- a/frontend/src/global_styles/content/_table.sass +++ b/frontend/src/global_styles/content/_table.sass @@ -76,6 +76,7 @@ $input-elements: input, 'input.form--text-field', select, 'select.form--select', &.ng-leave @include animation(0.5s fade-out) + table.generic-table border-collapse: collapse width: 100% @@ -381,3 +382,8 @@ thead.-sticky th p:last-child margin-bottom: 0 + +.generic-table + &--action-menu-button.leading-visual-icon-header + position: relative + top: 2px From b4ea1901549735358de94d87bb542b485b4b91e0 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Tue, 10 Dec 2024 10:22:48 +0100 Subject: [PATCH 12/27] [#58160] fix existing sort helper specs --- spec/helpers/sort_helper_spec.rb | 36 ++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/spec/helpers/sort_helper_spec.rb b/spec/helpers/sort_helper_spec.rb index 1c4e9b2ff5f7..5b5dec2210c6 100644 --- a/spec/helpers/sort_helper_spec.rb +++ b/spec/helpers/sort_helper_spec.rb @@ -281,11 +281,7 @@ def session; @session ||= {}; end end describe "#sort_header_with_action_menu" do - subject(:output) do - helper.sort_header_with_action_menu("id", - %w[name id description], {}, **options) - end - + let(:id_column) { Queries::Projects::Selects::Default.new "id" } let(:options) { { param: :json, sortable: true } } let(:sort_criteria) { SortHelper::SortCriteria.new } @@ -295,6 +291,11 @@ def session; @session ||= {}; end Nokogiri::HTML(output).at_css("th .generic-table--sort-header action-menu") end + subject(:output) do + helper.sort_header_with_action_menu(id_column, + %w[name id description], {}, **options) + end + before do # helper relies on this instance var @sort_criteria = sort_criteria @@ -343,7 +344,7 @@ def session; @session ||= {}; end context "with the current column being the leftmost one" do subject(:output) do - helper.sort_header_with_action_menu("id", + helper.sort_header_with_action_menu(id_column, %w[id name description], {}, **options) end @@ -359,7 +360,7 @@ def session; @session ||= {}; end context "with the current column being the rightmost one" do subject(:output) do - helper.sort_header_with_action_menu("id", + helper.sort_header_with_action_menu(id_column, %w[name description id], {}, **options) end @@ -397,7 +398,7 @@ def session; @session ||= {}; end context "with a filter mapping for the column" do subject(:output) do - helper.sort_header_with_action_menu("id", + helper.sort_header_with_action_menu(id_column, %w[name id description], { "id" => "id_code" }, **options) end @@ -413,7 +414,7 @@ def session; @session ||= {}; end context "with the filter mapping specifying there is no filter for the column" do subject(:output) do # With the filter name mapped to nil, we expect no filter action to be present. - helper.sort_header_with_action_menu("id", + helper.sort_header_with_action_menu(id_column, %w[name id description], { "id" => nil }, **options) end @@ -422,5 +423,22 @@ def session; @session ||= {}; end expect(filter_by).to be_nil end end + + context "with a life cycle column" do + let(:life_cycle_step) { create(:project_gate_definition) } + let(:life_cycle_column) { Queries::Projects::Selects::LifeCycleStep.new("lcsd_#{life_cycle_step.id}") } + + subject(:output) do + # Not setting any filter column mappings here, so for other column types, this should use the default filter + helper.sort_header_with_action_menu(life_cycle_column, + %W[name lcsd_#{life_cycle_step.id}], {}, **options) + end + + it "never offers a filter by action" do + # But a life cycle column never offers a filter (until #59183 is implemented) + filter_by = action_menu.at_css("action-list .ActionListItem button[data-test-selector='id-filter-by']") + expect(filter_by).to be_nil + end + end end end From b3b345249ff1dd366b7db7495ab93dc3f80cff24 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Tue, 10 Dec 2024 11:28:10 +0100 Subject: [PATCH 13/27] [#58160] remove outdated browser CSS fix Following the link, firefox now supports this feature. --- .../global_styles/content/_projects_list.sass | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/frontend/src/global_styles/content/_projects_list.sass b/frontend/src/global_styles/content/_projects_list.sass index 0260b2f463c1..93fa5bc3a729 100644 --- a/frontend/src/global_styles/content/_projects_list.sass +++ b/frontend/src/global_styles/content/_projects_list.sass @@ -104,19 +104,3 @@ $content-padding: 10px padding-left: 1em li list-style-type: none - -// Firefox does not support the popover attribute yet. Thus, the Primer action menus create an ugly scroll inside the table -// Follow https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover#browser_compatibility to see when this can be removed -body:not(.-browser-firefox) - .project-list-page - display: flex - flex-direction: column - max-height: calc(100vh - #{var(--header-height)} - #{$content-padding} - #{$content-padding}) - - &--table - display: flex - flex-grow: 1 - overflow: auto - - .generic-table--results-container - overflow-x: hidden From 2dacd2a473f9134298d9bb1d9f95b38fde01eaf1 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 11 Dec 2024 14:50:19 +0100 Subject: [PATCH 14/27] [#58160] spec for project index page --- .../commit-msg-prefix-from-branch.sh | 35 +++++++++ app/models/project/life_cycle_step.rb | 5 ++ .../project/life_cycle_step_definition.rb | 4 ++ .../projects/selects/life_cycle_step.rb | 10 ++- spec/features/projects/projects_index_spec.rb | 71 +++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100755 .lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh diff --git a/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh b/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh new file mode 100755 index 000000000000..4aa681513466 --- /dev/null +++ b/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Looks at the current branch name and tries to extract the correct work package number from it. +# If found, the commit message will automatically be prefixed with the work package number, following this format: +# +# [#58160] +# + +# File path for the commit message +COMMIT_MSG_FILE=$1 + +# Branch name +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# Debugging logs +#echo "Hook triggered for branch: $BRANCH_NAME" >> /tmp/hook_debug.log +#echo "Commit message file: $COMMIT_MSG_FILE" >> /tmp/hook_debug.log + +# Extract work package number from branch name (e.g., feature/58160 or bug/59977) +if [[ $BRANCH_NAME =~ (feature|bug)/([0-9]+) ]]; then + WP_NUMBER=${BASH_REMATCH[2]} + PREFIX="[#$WP_NUMBER] " +# echo "Detected issue number: $WP_NUMBER" >> /tmp/hook_debug.log + + # Filter out comment lines and leading whitespace for the check + FILTERED_MESSAGE=$(grep -v '^#' "$COMMIT_MSG_FILE" | sed 's/^[[:space:]]*//') + + # Log filtered message for debugging +# echo "Filtered commit message:" >> /tmp/hook_debug.log +# echo "$FILTERED_MESSAGE" >> /tmp/hook_debug.log + + # Check if the prefix is present in the actual commit message + if [[ "$FILTERED_MESSAGE" != "$PREFIX"* ]]; then + echo "$PREFIX$(cat "$COMMIT_MSG_FILE")" > "$COMMIT_MSG_FILE" + fi +fi diff --git a/app/models/project/life_cycle_step.rb b/app/models/project/life_cycle_step.rb index dfa4b11efda9..d4ffcf438427 100644 --- a/app/models/project/life_cycle_step.rb +++ b/app/models/project/life_cycle_step.rb @@ -46,4 +46,9 @@ def initialize(*args) super end + + def column_name + # The id of the associated definition is relevant for displaying the correct column headers + "lcsd_#{definition_id}" + end end diff --git a/app/models/project/life_cycle_step_definition.rb b/app/models/project/life_cycle_step_definition.rb index 48e8e00d94bf..5f1181f326d0 100644 --- a/app/models/project/life_cycle_step_definition.rb +++ b/app/models/project/life_cycle_step_definition.rb @@ -55,4 +55,8 @@ def initialize(*args) def step_class raise NotImplementedError end + + def column_name + "lcsd_#{id}" + end end diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index a756695d4f46..7a055a7d30fe 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -60,7 +60,15 @@ def available? def action_menu_header(button) # Show the proper icon for the definition with the correct color. - icon = life_cycle_step.is_a?(Project::StageDefinition) ? :"git-commit" : :diamond + icon = case life_cycle_step + when Project::StageDefinition + :"git-commit" + when Project::GateDefinition + :diamond + else + raise "Unknown life cycle step: #{life_cycle_step}" + end + classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step) button.with_leading_visual_icon(icon:, classes:) diff --git a/spec/features/projects/projects_index_spec.rb b/spec/features/projects/projects_index_spec.rb index 5e375ac252b8..522998574e3a 100644 --- a/spec/features/projects/projects_index_spec.rb +++ b/spec/features/projects/projects_index_spec.rb @@ -329,6 +329,77 @@ def load_and_open_filters(user) end end + context "with life cycle columns" do + shared_let(:life_cycle_gate) { create(:project_gate, date: Date.new(2024, 12, 13)) } + shared_let(:life_cycle_stage) do + create(:project_stage, + start_date: Date.new(2024, 12, 1), + end_date: Date.new(2024, 12, 13)) + end + shared_let(:inactive_life_cycle_gate) { create(:project_gate, active: false) } + shared_let(:inactive_life_cycle_stage) { create(:project_stage, active: false) } + + specify "life cycle columns do not show up by default" do + login_as(admin) + projects_page.visit! + + expect(page).to have_no_text(life_cycle_gate.definition.name.upcase) + expect(page).to have_no_text(life_cycle_stage.definition.name.upcase) + expect(page).to have_no_text(inactive_life_cycle_gate.definition.name.upcase) + expect(page).to have_no_text(inactive_life_cycle_stage.definition.name.upcase) + end + + specify "life cycle columns show up when configured to do so" do + login_as(admin) + projects_page.visit! + + projects_page.expect_columns("Name") + projects_page.set_columns(life_cycle_gate.definition.name) + + expect(page).to have_text(life_cycle_gate.definition.name.upcase) + end + + specify "inactive life cycle columns have no cell content" do + login_as(admin) + projects_page.visit! + + projects_page.expect_columns("Name") + + col_names = [life_cycle_gate, life_cycle_stage, + inactive_life_cycle_gate, + inactive_life_cycle_stage].map { |l| l.definition.name } + + projects_page.set_columns(*col_names) + # Inactive columns are still displayed in the header: + projects_page.expect_columns("Name", *col_names) + + gate_project = life_cycle_gate.project + projects_page.within_row(gate_project) do + expect(page).to have_css(".name", text: gate_project.name) + expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "12/13/2024") + # life cycle assigned to other project, no text here + expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "") + # inactive life cycles, no text here + expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") + expect(page).to have_css(".lcsd_#{inactive_life_cycle_gate.definition.id}", text: "") + end + + stage_project = life_cycle_stage.project + projects_page.within_row(stage_project) do + expect(page).to have_css(".name", text: stage_project.name) + expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "12/01/2024 - 12/13/2024") + # life cycle assigned to other project, no text here + expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "") + end + + # Inactive life cycle steps never show their date values + other_proj = inactive_life_cycle_stage.project + projects_page.within_row(other_proj) do + expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") + end + end + end + context "with valid Enterprise token" do shared_let(:long_text_custom_field) { create(:text_project_custom_field) } specify "CF columns and filters are not visible by default" do From 0052ff51a33e226218c4b883644957fb5e95e7d4 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Wed, 11 Dec 2024 14:55:04 +0100 Subject: [PATCH 15/27] [#58160] fix rubocop issue --- app/helpers/sort_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index 844c9a5b5679..3158677cbadb 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -392,7 +392,6 @@ def find_filter_for_column(column, filter_mapping) # Some of the method arguments are only needed for specific actions. def action_menu(column, table_columns, caption, default_order, filter_column_mapping = {}, allowed_params: nil, **html_options) - attribute = column.attribute caption ||= attribute.to_s.humanize From 9029c0db2dfca233b417192fd33e35f8d8130007 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Thu, 12 Dec 2024 10:27:48 +0100 Subject: [PATCH 16/27] [#58160] refactor --- app/components/projects/row_component.rb | 3 +-- .../projects/selects/life_cycle_step.rb | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 4bae6784d01c..80d6e4456002 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -99,10 +99,9 @@ def custom_field_column(column) def life_cycle_step_column(column) return nil unless user_can_view_project? - # FIXME: make this efficient ls = Project::LifeCycleStep .where(active: true) - .find_by(definition_id: column.life_cycle_step.id, project:) + .find_by(definition_id: column.life_cycle.id, project:) return nil if ls.blank? diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 7a055a7d30fe..028b9ad72c05 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -44,32 +44,32 @@ def self.all_available end def caption - life_cycle_step.name + life_cycle.name end - def life_cycle_step - return @life_cycle_step if defined?(@life_cycle_step) + def life_cycle + return @life_cycle if defined?(@life_cycle) - @life_cycle_step = Project::LifeCycleStepDefinition - .find_by(id: attribute[KEY, 1]) + @life_cycle = Project::LifeCycleStepDefinition + .find_by(id: attribute[KEY, 1]) end def available? - life_cycle_step.present? + life_cycle.present? end def action_menu_header(button) # Show the proper icon for the definition with the correct color. - icon = case life_cycle_step + icon = case life_cycle when Project::StageDefinition :"git-commit" when Project::GateDefinition :diamond else - raise "Unknown life cycle step: #{life_cycle_step}" + raise "Unknown life cycle definition for: #{life_cycle}" end - classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step) + classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle) button.with_leading_visual_icon(icon:, classes:) # As all other action menu headers, we will show an action icon and the caption: From d960a59b6660097b203419f4f913f0d5325ac0f9 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 09:31:28 +0100 Subject: [PATCH 17/27] [#58160] remove extra empty line --- frontend/src/global_styles/content/_table.sass | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/global_styles/content/_table.sass b/frontend/src/global_styles/content/_table.sass index c3699bdbb48b..9b964a9f9cee 100644 --- a/frontend/src/global_styles/content/_table.sass +++ b/frontend/src/global_styles/content/_table.sass @@ -76,7 +76,6 @@ $input-elements: input, 'input.form--text-field', select, 'select.form--select', &.ng-leave @include animation(0.5s fade-out) - table.generic-table border-collapse: collapse width: 100% From f3a8d06403d60afd83121be9fb768c0bf9141208 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 10:01:29 +0100 Subject: [PATCH 18/27] [#58160] avoid expensive queries --- app/components/projects/row_component.rb | 11 +++++------ app/components/projects/table_component.rb | 8 ++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 80d6e4456002..b6c2f49a432b 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -29,8 +29,9 @@ #++ module Projects class RowComponent < ::RowComponent - delegate :favored_project_ids, to: :table delegate :identifier, to: :project + delegate :favored_project_ids, to: :table + delegate :project_life_cycle_step_by_definition, to: :table def project model.first @@ -99,13 +100,11 @@ def custom_field_column(column) def life_cycle_step_column(column) return nil unless user_can_view_project? - ls = Project::LifeCycleStep - .where(active: true) - .find_by(definition_id: column.life_cycle.id, project:) + life_cycle_step = project_life_cycle_step_by_definition(column.life_cycle, project) - return nil if ls.blank? + return nil if life_cycle_step.blank? - fmt_date_or_range(ls.start_date, ls.end_date) + fmt_date_or_range(life_cycle_step.start_date, life_cycle_step.end_date) end def created_at diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index 7958c81df2ad..894bd45ab37a 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -195,6 +195,14 @@ def favored_project_ids @favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id) end + def project_life_cycle_step_by_definition(definition, project) + @project_life_cycle_steps_by_definition ||= Project::LifeCycleStep + .where(active: true) + .group_by { |s| [s.definition_id, s.project_id] } + + @project_life_cycle_steps_by_definition[[definition.id, project.id]]&.first + end + def sorted_by_lft? query.orders.first&.attribute == :lft end From fdb32f176c690ff07bd50789760b0ae6ded73178 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 10:04:24 +0100 Subject: [PATCH 19/27] [#58160] rename method --- app/components/projects/row_component.rb | 2 +- .../projects/selects/life_cycle_step.rb | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index b6c2f49a432b..67cb4292198f 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -100,7 +100,7 @@ def custom_field_column(column) def life_cycle_step_column(column) return nil unless user_can_view_project? - life_cycle_step = project_life_cycle_step_by_definition(column.life_cycle, project) + life_cycle_step = project_life_cycle_step_by_definition(column.life_cycle_step_definition, project) return nil if life_cycle_step.blank? diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 028b9ad72c05..47d91507e7d6 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -44,32 +44,32 @@ def self.all_available end def caption - life_cycle.name + life_cycle_step_definition.name end - def life_cycle - return @life_cycle if defined?(@life_cycle) + def life_cycle_step_definition + return @life_cycle_step_definition if defined?(@life_cycle_step_definition) - @life_cycle = Project::LifeCycleStepDefinition - .find_by(id: attribute[KEY, 1]) + @life_cycle_step_definition = Project::LifeCycleStepDefinition + .find_by(id: attribute[KEY, 1]) end def available? - life_cycle.present? + life_cycle_step_definition.present? end def action_menu_header(button) # Show the proper icon for the definition with the correct color. - icon = case life_cycle + icon = case life_cycle_step_definition when Project::StageDefinition :"git-commit" when Project::GateDefinition :diamond else - raise "Unknown life cycle definition for: #{life_cycle}" + raise "Unknown life cycle definition for: #{life_cycle_step_definition}" end - classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle) + classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step_definition) button.with_leading_visual_icon(icon:, classes:) # As all other action menu headers, we will show an action icon and the caption: From 21cea8ad21bd7450ee56f5960d2d52be6d73d03a Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 10:12:32 +0100 Subject: [PATCH 20/27] [#58160] move UI logic out of the Select This also simplifies the sort_helper neatly. --- app/helpers/sort_helper.rb | 3 +-- app/models/queries/projects/selects/life_cycle_step.rb | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/helpers/sort_helper.rb b/app/helpers/sort_helper.rb index 3158677cbadb..8a5bc7dfafc3 100644 --- a/app/helpers/sort_helper.rb +++ b/app/helpers/sort_helper.rb @@ -427,9 +427,8 @@ def action_button(menu, column, caption, favorite: false) if favorite # This column only shows an icon, no text. render Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, "aria-label": I18n.t(:label_favorite)) - elsif column.respond_to?(:action_menu_header) - column.action_menu_header(button) else + button.with_leading_visual_icon(**column.visual_icon) if column.respond_to?(:visual_icon) button.with_trailing_action_icon(icon: :"triangle-down") h(caption).to_s diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 47d91507e7d6..a2190ca0a7f3 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -58,7 +58,7 @@ def available? life_cycle_step_definition.present? end - def action_menu_header(button) + def visual_icon # Show the proper icon for the definition with the correct color. icon = case life_cycle_step_definition when Project::StageDefinition @@ -70,12 +70,8 @@ def action_menu_header(button) end classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step_definition) - button.with_leading_visual_icon(icon:, classes:) - # As all other action menu headers, we will show an action icon and the caption: - button.with_trailing_action_icon(icon: :"triangle-down") - - caption.to_s + { icon:, classes: } end def action_menu_classes From 32e804da19d09c7c1a1564c83aba37aac5c680c1 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 10:39:19 +0100 Subject: [PATCH 21/27] [#58160] provide specs for leading visual icon in action menu --- spec/helpers/sort_helper_spec.rb | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/spec/helpers/sort_helper_spec.rb b/spec/helpers/sort_helper_spec.rb index 5b5dec2210c6..149da1f15bab 100644 --- a/spec/helpers/sort_helper_spec.rb +++ b/spec/helpers/sort_helper_spec.rb @@ -310,6 +310,10 @@ def session; @session ||= {}; end expect(action_menu.at_css("button#menu-id-button .Button-content .Button-label").text).to eq("Id") end + it "does not render an icon by default" do + expect(action_menu.at_css(".generic-table--action-menu-button .Button-leadingVisual")).to be_blank + end + it "shows sorting actions in the action-menu" do sort_desc = action_menu.at_css("action-list .ActionListItem a[data-test-selector='id-sort-desc']") expect(sort_desc.at_css(".ActionListItem-label").text.strip).to eq("Sort descending") @@ -424,10 +428,12 @@ def session; @session ||= {}; end end end - context "with a life cycle column" do + context "with a life cycle gate column" do let(:life_cycle_step) { create(:project_gate_definition) } let(:life_cycle_column) { Queries::Projects::Selects::LifeCycleStep.new("lcsd_#{life_cycle_step.id}") } + let(:options) { { caption: life_cycle_step.name } } + subject(:output) do # Not setting any filter column mappings here, so for other column types, this should use the default filter helper.sort_header_with_action_menu(life_cycle_column, @@ -439,6 +445,26 @@ def session; @session ||= {}; end filter_by = action_menu.at_css("action-list .ActionListItem button[data-test-selector='id-filter-by']") expect(filter_by).to be_nil end + + it "shows a diamond icon in the header for gates" do + icon = action_menu.at_css(".generic-table--action-menu-button .Button-leadingVisual .octicon-diamond") + expect(icon).to be_present + + header_text = action_menu.at_css(".generic-table--action-menu-button .Button-label").text.strip + expect(header_text).to eq(life_cycle_column.caption) + end + + context "with a life cycle stage column" do + let(:life_cycle_step) { create(:project_stage_definition) } + + it "shows a commit icon in the header for gates" do + icon = action_menu.at_css(".generic-table--action-menu-button .Button-leadingVisual .octicon-git-commit") + expect(icon).to be_present + + header_text = action_menu.at_css(".generic-table--action-menu-button .Button-label").text.strip + expect(header_text).to eq(life_cycle_column.caption) + end + end end end end From 80d457380582c572394f37b7b5e50637b5e16e4b Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 15:24:11 +0100 Subject: [PATCH 22/27] [#58160] project list: check for stages and gates feature flag --- .../projects/selects/life_cycle_step.rb | 2 +- spec/features/projects/projects_index_spec.rb | 109 ++++++++++-------- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index a2190ca0a7f3..425b53588bcb 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -55,7 +55,7 @@ def life_cycle_step_definition end def available? - life_cycle_step_definition.present? + OpenProject::FeatureDecisions.stages_and_gates_active? && life_cycle_step_definition.present? end def visual_icon diff --git a/spec/features/projects/projects_index_spec.rb b/spec/features/projects/projects_index_spec.rb index 522998574e3a..999bfb9dd7f7 100644 --- a/spec/features/projects/projects_index_spec.rb +++ b/spec/features/projects/projects_index_spec.rb @@ -339,63 +339,78 @@ def load_and_open_filters(user) shared_let(:inactive_life_cycle_gate) { create(:project_gate, active: false) } shared_let(:inactive_life_cycle_stage) { create(:project_stage, active: false) } - specify "life cycle columns do not show up by default" do - login_as(admin) - projects_page.visit! + context "with the feature flag disabled", with_flag: { stages_and_gates: false } do + specify "life cycle columns cannot be configured to show up" do + login_as(admin) + projects_page.visit! - expect(page).to have_no_text(life_cycle_gate.definition.name.upcase) - expect(page).to have_no_text(life_cycle_stage.definition.name.upcase) - expect(page).to have_no_text(inactive_life_cycle_gate.definition.name.upcase) - expect(page).to have_no_text(inactive_life_cycle_stage.definition.name.upcase) - end + # Trying to set the column via configure view + projects_page.set_columns(life_cycle_gate.definition.name) - specify "life cycle columns show up when configured to do so" do - login_as(admin) - projects_page.visit! + # It didn't work + expect(page).to have_no_text(life_cycle_gate.definition.name.upcase) + end + end - projects_page.expect_columns("Name") - projects_page.set_columns(life_cycle_gate.definition.name) + context "with the feature flag enabled", with_flag: { stages_and_gates: true } do + specify "life cycle columns do not show up by default" do + login_as(admin) + projects_page.visit! - expect(page).to have_text(life_cycle_gate.definition.name.upcase) - end + expect(page).to have_no_text(life_cycle_gate.definition.name.upcase) + expect(page).to have_no_text(life_cycle_stage.definition.name.upcase) + expect(page).to have_no_text(inactive_life_cycle_gate.definition.name.upcase) + expect(page).to have_no_text(inactive_life_cycle_stage.definition.name.upcase) + end - specify "inactive life cycle columns have no cell content" do - login_as(admin) - projects_page.visit! + specify "life cycle columns show up when configured to do so" do + login_as(admin) + projects_page.visit! - projects_page.expect_columns("Name") + projects_page.expect_columns("Name") + projects_page.set_columns(life_cycle_gate.definition.name) - col_names = [life_cycle_gate, life_cycle_stage, - inactive_life_cycle_gate, - inactive_life_cycle_stage].map { |l| l.definition.name } - - projects_page.set_columns(*col_names) - # Inactive columns are still displayed in the header: - projects_page.expect_columns("Name", *col_names) - - gate_project = life_cycle_gate.project - projects_page.within_row(gate_project) do - expect(page).to have_css(".name", text: gate_project.name) - expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "12/13/2024") - # life cycle assigned to other project, no text here - expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "") - # inactive life cycles, no text here - expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") - expect(page).to have_css(".lcsd_#{inactive_life_cycle_gate.definition.id}", text: "") + expect(page).to have_text(life_cycle_gate.definition.name.upcase) end - stage_project = life_cycle_stage.project - projects_page.within_row(stage_project) do - expect(page).to have_css(".name", text: stage_project.name) - expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "12/01/2024 - 12/13/2024") - # life cycle assigned to other project, no text here - expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "") - end + specify "inactive life cycle columns have no cell content" do + login_as(admin) + projects_page.visit! + + projects_page.expect_columns("Name") + + col_names = [life_cycle_gate, life_cycle_stage, + inactive_life_cycle_gate, + inactive_life_cycle_stage].map { |l| l.definition.name } + + projects_page.set_columns(*col_names) + # Inactive columns are still displayed in the header: + projects_page.expect_columns("Name", *col_names) + + gate_project = life_cycle_gate.project + projects_page.within_row(gate_project) do + expect(page).to have_css(".name", text: gate_project.name) + expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "12/13/2024") + # life cycle assigned to other project, no text here + expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "") + # inactive life cycles, no text here + expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") + expect(page).to have_css(".lcsd_#{inactive_life_cycle_gate.definition.id}", text: "") + end - # Inactive life cycle steps never show their date values - other_proj = inactive_life_cycle_stage.project - projects_page.within_row(other_proj) do - expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") + stage_project = life_cycle_stage.project + projects_page.within_row(stage_project) do + expect(page).to have_css(".name", text: stage_project.name) + expect(page).to have_css(".lcsd_#{life_cycle_stage.definition.id}", text: "12/01/2024 - 12/13/2024") + # life cycle assigned to other project, no text here + expect(page).to have_css(".lcsd_#{life_cycle_gate.definition.id}", text: "") + end + + # Inactive life cycle steps never show their date values + other_proj = inactive_life_cycle_stage.project + projects_page.within_row(other_proj) do + expect(page).to have_css(".lcsd_#{inactive_life_cycle_stage.definition.id}", text: "") + end end end end From 15ee44d566b2ac76d2bdad8755599f0aee339468 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Fri, 13 Dec 2024 15:26:21 +0100 Subject: [PATCH 23/27] [#58160] ignore local lefthook files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 0a27bf8e565e..a677a999f576 100644 --- a/.gitignore +++ b/.gitignore @@ -134,6 +134,8 @@ structure.sql lefthook-local.yml .rubocop-local.yml +/.lefthook-local/ + frontend/package-lock.json # Testing and nextcloud infrastructure From 68dc0923586d1d3eefc69fcd19c221b88b86dfed Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 16 Dec 2024 09:16:59 +0100 Subject: [PATCH 24/27] [#58160] Use `index_by` to fetch single value per key --- app/components/projects/table_component.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index 894bd45ab37a..e7d8d3da22b8 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -198,9 +198,9 @@ def favored_project_ids def project_life_cycle_step_by_definition(definition, project) @project_life_cycle_steps_by_definition ||= Project::LifeCycleStep .where(active: true) - .group_by { |s| [s.definition_id, s.project_id] } + .index_by { |s| [s.definition_id, s.project_id] } - @project_life_cycle_steps_by_definition[[definition.id, project.id]]&.first + @project_life_cycle_steps_by_definition[[definition.id, project.id]] end def sorted_by_lft? From 049157468485b8dc6ff0d5d694f99253ddea7846 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 16 Dec 2024 10:00:22 +0100 Subject: [PATCH 25/27] [#58160] Remove local file --- .../commit-msg-prefix-from-branch.sh | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100755 .lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh diff --git a/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh b/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh deleted file mode 100755 index 4aa681513466..000000000000 --- a/.lefthook-local/prepare-commit-msg/commit-msg-prefix-from-branch.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -# Looks at the current branch name and tries to extract the correct work package number from it. -# If found, the commit message will automatically be prefixed with the work package number, following this format: -# -# [#58160] -# - -# File path for the commit message -COMMIT_MSG_FILE=$1 - -# Branch name -BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - -# Debugging logs -#echo "Hook triggered for branch: $BRANCH_NAME" >> /tmp/hook_debug.log -#echo "Commit message file: $COMMIT_MSG_FILE" >> /tmp/hook_debug.log - -# Extract work package number from branch name (e.g., feature/58160 or bug/59977) -if [[ $BRANCH_NAME =~ (feature|bug)/([0-9]+) ]]; then - WP_NUMBER=${BASH_REMATCH[2]} - PREFIX="[#$WP_NUMBER] " -# echo "Detected issue number: $WP_NUMBER" >> /tmp/hook_debug.log - - # Filter out comment lines and leading whitespace for the check - FILTERED_MESSAGE=$(grep -v '^#' "$COMMIT_MSG_FILE" | sed 's/^[[:space:]]*//') - - # Log filtered message for debugging -# echo "Filtered commit message:" >> /tmp/hook_debug.log -# echo "$FILTERED_MESSAGE" >> /tmp/hook_debug.log - - # Check if the prefix is present in the actual commit message - if [[ "$FILTERED_MESSAGE" != "$PREFIX"* ]]; then - echo "$PREFIX$(cat "$COMMIT_MSG_FILE")" > "$COMMIT_MSG_FILE" - fi -fi From 5baa171994d7a3ec0d6eb210b2448d94cf0b43cb Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 16 Dec 2024 11:50:18 +0100 Subject: [PATCH 26/27] [#58160] fix call to available? --- app/models/queries/projects/selects/life_cycle_step.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/queries/projects/selects/life_cycle_step.rb b/app/models/queries/projects/selects/life_cycle_step.rb index 425b53588bcb..cd591ed06209 100644 --- a/app/models/queries/projects/selects/life_cycle_step.rb +++ b/app/models/queries/projects/selects/life_cycle_step.rb @@ -54,8 +54,12 @@ def life_cycle_step_definition .find_by(id: attribute[KEY, 1]) end + def self.available? + OpenProject::FeatureDecisions.stages_and_gates_active? + end + def available? - OpenProject::FeatureDecisions.stages_and_gates_active? && life_cycle_step_definition.present? + life_cycle_step_definition.present? end def visual_icon From 31ba520a70bef73df2a1632e1f190c3d9187ff51 Mon Sep 17 00:00:00 2001 From: Tobias Dillmann Date: Mon, 16 Dec 2024 12:21:49 +0100 Subject: [PATCH 27/27] [#58160] fix up broken spec --- spec/features/projects/projects_index_spec.rb | 13 ++++++++----- spec/support/pages/projects/index.rb | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/spec/features/projects/projects_index_spec.rb b/spec/features/projects/projects_index_spec.rb index 999bfb9dd7f7..ea4ba90c2d87 100644 --- a/spec/features/projects/projects_index_spec.rb +++ b/spec/features/projects/projects_index_spec.rb @@ -344,11 +344,14 @@ def load_and_open_filters(user) login_as(admin) projects_page.visit! - # Trying to set the column via configure view - projects_page.set_columns(life_cycle_gate.definition.name) - - # It didn't work - expect(page).to have_no_text(life_cycle_gate.definition.name.upcase) + element_selector = "#columns-select_autocompleter ng-select.op-draggable-autocomplete--input" + results_selector = "#columns-select_autocompleter ng-dropdown-panel .ng-dropdown-panel-items" + projects_page.expect_no_config_columns(life_cycle_gate.definition.name, + life_cycle_stage.definition.name, + inactive_life_cycle_gate.definition.name, + inactive_life_cycle_stage.definition.name, + element_selector:, + results_selector:) end end diff --git a/spec/support/pages/projects/index.rb b/spec/support/pages/projects/index.rb index 54f3111765f0..69034ae8c4b4 100644 --- a/spec/support/pages/projects/index.rb +++ b/spec/support/pages/projects/index.rb @@ -398,13 +398,14 @@ def set_columns(*columns) wait_for_network_idle end - def expect_no_config_columns(*columns) + def expect_no_config_columns(*columns, element_selector: ".op-draggable-autocomplete--input", + results_selector: ".ng-dropdown-panel-items") open_configure_view columns.each do |column| - expect_no_ng_option find(".op-draggable-autocomplete--input"), + expect_no_ng_option find(element_selector), column, - results_selector: ".ng-dropdown-panel-items" + results_selector: end within "dialog" do