Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#58160] project stage columns on project list #17400

Merged
merged 27 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b47ce31
[#58160] project list: query for life cycle steps
EinLama Dec 4, 2024
3796f8f
[#58160] project list: show life cycle name in column for now
EinLama Dec 5, 2024
a22cf91
[#58160] show lifecycle dates in row
EinLama Dec 5, 2024
f64d6e7
[#58160] use formatting helper
EinLama Dec 5, 2024
47ec2bf
[#58160] refactor helper
EinLama Dec 6, 2024
5e9fa54
[#58160] show correct step for project in table
EinLama Dec 9, 2024
36de780
[#58160] disable filter column for life cycle steps
EinLama Dec 9, 2024
69047d5
[#58160] use filter_map instead
EinLama Dec 9, 2024
3ccf625
[#58160] show icons in life cycle list headers
EinLama Dec 9, 2024
bf6ef1a
[#58160] apply the correct color to life cycle header icons
EinLama Dec 9, 2024
6a51ac4
[#58160] fix visual glitch with leading icon in action button
EinLama Dec 9, 2024
b4ea190
[#58160] fix existing sort helper specs
EinLama Dec 10, 2024
b3b3452
[#58160] remove outdated browser CSS fix
EinLama Dec 10, 2024
2dacd2a
[#58160] spec for project index page
EinLama Dec 11, 2024
0052ff5
[#58160] fix rubocop issue
EinLama Dec 11, 2024
9029c0d
[#58160] refactor
EinLama Dec 12, 2024
d960a59
[#58160] remove extra empty line
EinLama Dec 13, 2024
f3a8d06
[#58160] avoid expensive queries
EinLama Dec 13, 2024
fdb32f1
[#58160] rename method
EinLama Dec 13, 2024
21cea8a
[#58160] move UI logic out of the Select
EinLama Dec 13, 2024
32e804d
[#58160] provide specs for leading visual icon in action menu
EinLama Dec 13, 2024
80d4573
[#58160] project list: check for stages and gates feature flag
EinLama Dec 13, 2024
15ee44d
[#58160] ignore local lefthook files
EinLama Dec 13, 2024
68dc092
[#58160] Use `index_by` to fetch single value per key
EinLama Dec 16, 2024
0491574
[#58160] Remove local file
EinLama Dec 16, 2024
5baa171
[#58160] fix call to available?
EinLama Dec 16, 2024
31ba520
[#58160] fix up broken spec
EinLama Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ structure.sql
lefthook-local.yml
.rubocop-local.yml

/.lefthook-local/

frontend/package-lock.json

# Testing and nextcloud infrastructure
Expand Down
36 changes: 35 additions & 1 deletion app/components/projects/row_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +70,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
Expand All @@ -94,6 +97,16 @@ def custom_field_column(column)
end
end

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_step_definition, project)

return nil if life_cycle_step.blank?

fmt_date_or_range(life_cycle_step.start_date, life_cycle_step.end_date)
end

def created_at
helpers.format_date(project.created_at)
end
Expand Down Expand Up @@ -374,8 +387,29 @@ 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

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
# fmt_date_or_range(Date.new(2024, 12, 4))
# "04/12/2024"
#
# fmt_date_or_range(Date.new(2024, 12, 4), Date.new(2024, 12, 10))
# "04/12/2024 - 10/12/2024"
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
2 changes: 1 addition & 1 deletion app/components/projects/table_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ See COPYRIGHT and LICENSE files for more details.
</th>
<% 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 %>
<th>
<div class="generic-table--sort-header-outer">
Expand Down
8 changes: 8 additions & 0 deletions app/components/projects/table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
.index_by { |s| [s.definition_id, s.project_id] }

@project_life_cycle_steps_by_definition[[definition.id, project.id]]
end

def sorted_by_lft?
query.orders.first&.attribute == :lft
end
Expand Down
3 changes: 1 addition & 2 deletions app/helpers/projects_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 22 additions & 12 deletions app/helpers/sort_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -389,36 +392,43 @@ 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
attribute = column.attribute
caption ||= attribute.to_s.humanize

filter = find_filter_for_column(column, filter_column_mapping)
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`.
# It is important for keeping the current state in the GET parameters of each link used in
# 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)
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))
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
Expand Down
5 changes: 5 additions & 0 deletions app/models/project/life_cycle_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions app/models/project/life_cycle_step_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ def initialize(*args)
def step_class
raise NotImplementedError
end

def column_name
"lcsd_#{id}"
end
end
1 change: 1 addition & 0 deletions app/models/queries/projects.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions app/models/queries/projects/selects/life_cycle_step.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 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 = /\Alcsd_(\d+)\z/

def self.key
KEY
end

def self.all_available
return [] unless available?

Project::LifeCycleStepDefinition
.pluck(:id)
.map { |id| new(:"lcsd_#{id}") }
end

def caption
life_cycle_step_definition.name
end

def life_cycle_step_definition
return @life_cycle_step_definition if defined?(@life_cycle_step_definition)

@life_cycle_step_definition = Project::LifeCycleStepDefinition
.find_by(id: attribute[KEY, 1])
end

def self.available?
OpenProject::FeatureDecisions.stages_and_gates_active?
end

def available?
life_cycle_step_definition.present?
end

def visual_icon
# Show the proper icon for the definition with the correct color.
icon = case life_cycle_step_definition
when Project::StageDefinition
:"git-commit"
when Project::GateDefinition
:diamond
else
raise "Unknown life cycle definition for: #{life_cycle_step_definition}"
end

classes = helpers.hl_inline_class("life_cycle_step_definition", life_cycle_step_definition)

{ icon:, classes: }
end

def action_menu_classes
"leading-visual-icon-header"
end

private

def helpers
@helpers ||= Object.new.extend(ColorsHelper)
end
end
16 changes: 0 additions & 16 deletions frontend/src/global_styles/content/_projects_list.sass
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions frontend/src/global_styles/content/_table.sass
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,8 @@ thead.-sticky th

p:last-child
margin-bottom: 0

.generic-table
&--action-menu-button.leading-visual-icon-header
position: relative
top: 2px
Loading
Loading