Files
openproject/app/components/projects/row_component.rb
T
Ivan Kuchin 43a1df6684 replace formatted argument with format for attribute component
It was unclear whether formatted means already formatted or that it
should be, hopefully naming it as an action will be clearer
2026-02-05 14:15:12 +01:00

460 lines
14 KiB
Ruby

# 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 Projects
class RowComponent < ::RowComponent
include CalculatedValues::ErrorsHelper
delegate :identifier, to: :project
delegate :favorited_project_ids,
:project_phase_by_definition,
to: :table
def project
model.first
end
def level
model.last
end
# Hierarchy cell is just a placeholder
def hierarchy
""
end
def favorited # rubocop:disable Metrics/AbcSize
return nil if project.archived?
render(Primer::Beta::IconButton.new(
icon: currently_favorited? ? "star-fill" : "star",
scheme: :invisible,
mobile_icon: currently_favorited? ? "star-fill" : "star",
size: :medium,
tag: :a,
tooltip_direction: :e,
href: helpers.build_favorite_path(project, format: :html),
data: { turbo_method: currently_favorited? ? :delete : :post },
classes: currently_favorited? ? "op-primer--star-icon " : "op-project-row-component--favorite",
label: currently_favorited? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
aria: { label: currently_favorited? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
test_selector: "project-list-favorite-button"
))
end
def currently_favorited?
@currently_favorited ||= favorited_project_ids.include?(project.id)
end
def column_value(column)
if custom_field_column?(column)
custom_field_column(column)
elsif project_phase_column?(column)
project_phase_column(column)
else
send(column.attribute)
end
end
def custom_field_column(column) # rubocop:disable Metrics/AbcSize
return nil unless user_can_view_project_attributes?
cf = column.custom_field
custom_value = project.formatted_custom_value_for(cf)
if cf.field_format == "text" && custom_value.present?
render OpenProject::Common::AttributeComponent.new(
"dialog-#{project.id}-cf-#{cf.id}",
cf.name,
custom_value,
format: false # already formatted
)
elsif custom_value.is_a?(Array)
safe_join(Array(custom_value).compact_blank, ", ")
elsif cf.calculated_value?
render_calculated_value(cf, custom_value)
else
custom_value
end
end
def render_calculated_value(custom_field, custom_value)
if (error = custom_field.first_calculation_error(project))
render(Primer::Alpha::Dialog.new(title: I18n.t("calculated_values.error_dialog.title"),
data: {
test_selector: "calculated-value-error-dialog-#{custom_field.id}"
})) do |dialog|
dialog.with_show_button(icon: "alert-fill",
"aria-label": I18n.t("calculated_values.error_dialog.title"),
data: { test_selector: "calculated-value-error-btn-#{custom_field.id}" },
scheme: :invisible)
dialog.with_body { calculated_value_error_msg(error) }
end
else
custom_value
end
end
def project_phase_column(column)
return nil unless user_can_view_project_phases?
phase = project_phase_by_definition(column.project_phase_definition, project)
return nil if phase.blank?
render Projects::PhaseComponent.new(phase:)
end
def created_at
helpers.format_date(project.created_at)
end
def latest_activity_at
helpers.format_date(project.latest_activity_at)
end
def updated_at
helpers.format_date(project.updated_at)
end
def required_disk_space
return "" unless project.required_disk_space.to_i > 0
number_to_human_size(project.required_disk_space, precision: 2)
end
def id
project.id.to_s
end
def name
content = [
hierarchy_icon,
name_link_section,
archived_label,
workspace_type_badge
].compact_blank
content_tag(:div, safe_join(content), class: "projects-table--name")
end
def hierarchy_icon
content_tag(:i, "", class: "projects-table--hierarchy-icon")
end
def name_link_section
content_tag(:span, class: "projects-table--name-text") do
helpers.link_to_project(project, {}, { data: { turbo: false } }, false)
end
end
def workspace_type_badge
return unless OpenProject::FeatureDecisions.portfolio_models_active?
# Only show icon and type for non-project workspaces
return unless project.workspace_type.in?(["portfolio", "program"])
render(Primer::Beta::Text.new(classes: "projects-table--name-description")) do
icon = render(Primer::Beta::Octicon.new(
icon: helpers.workspace_icon(project.workspace_type),
size: :xsmall
))
safe_join([icon, " ", I18n.t(:"label_#{project.workspace_type}")])
end
end
def archived_label
return unless project.archived?
content_tag(:span, "(#{I18n.t('project.archive.archived')})", class: "archived-label")
end
def project_status
return nil unless user_can_view_project_attributes?
content = "".html_safe
status_code = project.status_code
if status_code
classes = helpers.project_status_css_class(status_code)
content << content_tag(:span, "", class: "project-status--bulb -inline #{classes}")
content << content_tag(:span, helpers.project_status_name(status_code), class: "project-status--name #{classes}")
end
content
end
def status_explanation
return nil unless user_can_view_project_attributes?
if project.status_explanation.present? && project.status_explanation
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-status-explanation",
I18n.t("activerecord.attributes.project.status_explanation"),
project.status_explanation)
end
end
def description
return nil unless user_can_view_project_attributes?
if project.description.present?
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-description",
I18n.t("activerecord.attributes.project.description"),
project.description)
end
end
def public
helpers.checked_image project.public?
end
def row_css_class
classes = %w[basics context-menu--reveal op-project-row-component]
classes << project_css_classes
classes << row_css_level_classes
classes.join(" ")
end
def row_css_id
"project-#{project.id}"
end
def row_css_level_classes
if level > 0
"idnt idnt-#{level}"
else
""
end
end
def project_css_classes
s = " project ".html_safe
s << " root" if project.root?
s << " child" if project.child?
s << (project.leaf? ? " leaf" : " parent")
s
end
def column_css_class(column)
"#{column.attribute} #{additional_css_class(column)}"
end
def additional_css_class(column)
if column.attribute == :name
"project--hierarchy #{'archived' if project.archived?}"
elsif %i[status_explanation description].include?(column.attribute)
"project-long-text-container"
elsif column.attribute == :favorited
"-w-abs-45"
elsif custom_field_column?(column)
cf = column.custom_field
formattable = cf.field_format == "text" ? " project-long-text-container" : ""
"format-#{cf.field_format}#{formattable}"
end
end
def button_links
if more_menu_items.empty?
[]
else
[action_menu]
end
end
def action_menu
render(Primer::Alpha::ActionMenu.new(test_selector: "project-list-row--action-menu")) do |menu|
menu.with_show_button(scheme: :invisible,
size: :small,
icon: :"kebab-horizontal",
"aria-label": t(:label_open_menu),
tooltip_direction: :w)
more_menu_items.each do |action_options|
action_options => { scheme:, label:, icon:, **button_options }
menu.with_item(scheme:,
label:,
test_selector: "project-list-row--action-menu-item",
content_arguments: button_options) do |item|
item.with_leading_visual_icon(icon:) if icon
end
end
end
end
def more_menu_items
@more_menu_items ||= [more_menu_subproject_item,
more_menu_settings_item,
more_menu_activity_item,
more_menu_favorite_item,
more_menu_unfavorite_item,
more_menu_archive_item,
more_menu_unarchive_item,
more_menu_copy_item,
more_menu_delete_item].compact
end
def more_menu_favorite_item
return if currently_favorited? || project.archived?
{
scheme: :default,
icon: "star",
href: helpers.build_favorite_path(project, format: :html),
data: { "turbo-method": :post },
label: I18n.t(:button_favorite),
aria: { label: I18n.t(:button_favorite) }
}
end
def more_menu_unfavorite_item
return if !currently_favorited? || project.archived?
{
scheme: :default,
icon: "star-fill",
size: :medium,
href: helpers.build_favorite_path(project, format: :html),
data: { "turbo-method": :delete },
classes: "op-primer--star-icon",
label: I18n.t(:button_unfavorite),
aria: { label: I18n.t(:button_unfavorite) }
}
end
def more_menu_subproject_item
if User.current.allowed_in_project?(:add_subprojects, project)
{
scheme: :default,
icon: :plus,
label: I18n.t(:label_subproject_new),
href: new_project_path(parent_id: project.id)
}
end
end
def more_menu_settings_item
if User.current.allowed_in_project?({ controller: "/projects/settings/general", action: "show", project_id: project.id },
project)
{
scheme: :default,
icon: :gear,
label: I18n.t(:label_project_settings),
href: project_settings_general_path(project),
data: { turbo: false }
}
end
end
def more_menu_activity_item
if User.current.allowed_in_project?(:view_project_activity, project)
{
scheme: :default,
icon: :check,
label: I18n.t(:label_project_activity),
href: project_activity_index_path(project, event_types: ["project_details"])
}
end
end
def more_menu_archive_item
if User.current.allowed_in_project?(:archive_project, project) && project.active?
{
scheme: :default,
icon: :lock,
label: I18n.t(:button_archive),
href: project_archive_path(project, status: params[:status]),
data: {
turbo_method: :post,
turbo_confirm: t("project.archive.are_you_sure", name: project.name)
}
}
end
end
def more_menu_unarchive_item
if User.current.admin? && project.archived? && (project.parent.nil? || project.parent.active?)
{
scheme: :default,
icon: :unlock,
label: I18n.t(:button_unarchive),
href: project_archive_path(project, status: params[:status]),
data: { turbo_method: :delete }
}
end
end
def more_menu_copy_item
if User.current.allowed_in_project?(:copy_projects, project) && !project.archived?
{
scheme: :default,
icon: :copy,
label: I18n.t(:button_copy),
href: copy_project_path(project),
data: { turbo: false }
}
end
end
def more_menu_delete_item
if User.current.admin
{
scheme: :danger,
icon: :trash,
label: I18n.t(:button_delete),
href: confirm_destroy_project_path(project),
data: { turbo_stream: true }
}
end
end
def user_can_view_project_attributes?
User.current.allowed_in_project?(:view_project_attributes, project)
end
def user_can_view_project_phases?
User.current.allowed_in_project?(:view_project_phases, project)
end
def custom_field_column?(column)
column.is_a?(::Queries::Projects::Selects::CustomField)
end
def project_phase_column?(column)
column.is_a?(::Queries::Projects::Selects::ProjectPhase)
end
def current_page
table.model.current_page.to_s
end
end
end