Skip to content

Commit

Permalink
Merge pull request #17400 from opf/feature/58160-project-stage-column…
Browse files Browse the repository at this point in the history
…s-on-project-list

[#58160] project stage columns on project list
  • Loading branch information
EinLama authored Dec 16, 2024
2 parents fe66186 + 31ba520 commit a0734ad
Show file tree
Hide file tree
Showing 15 changed files with 320 additions and 44 deletions.
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

0 comments on commit a0734ad

Please sign in to comment.