Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3c0bd48
Add a checkbox for the project specific allow list on the custom fiel…
klaustopher Oct 23, 2026
fec780e
Build a stimulus controller that dynamically shows a danger dialog wh…
klaustopher Oct 23, 2026
4593c4e
Wire up the confirmation when disabling project specific settings
klaustopher Oct 23, 2026
816437a
Add new helpers do detect which projects use a custom field
klaustopher Oct 23, 2026
3f79a4d
Reintroduce the projects tab
klaustopher Oct 23, 2026
3fd55e9
Controller and routes to display project specific options
klaustopher Oct 23, 2026
6ab3df6
Allow setting up project specific custom values
klaustopher Oct 23, 2026
1e7b8cf
Use condensed mode for custom values table
klaustopher Oct 23, 2026
e5c1cde
Use the correct menu when managing hierarchy items
klaustopher Oct 23, 2026
f0cc0f4
Update texts for setting and modal
klaustopher Oct 26, 2026
a19adb7
Implement design changes
klaustopher Oct 26, 2026
6067cfa
Implement the projects tab as designed
klaustopher Oct 26, 2026
b81301c
Fix controller actions
klaustopher Oct 26, 2026
0a712cf
Add quick filter to show only projects that have project-specific values
klaustopher Oct 26, 2026
e92a78a
Qualify Projects superclasses in custom field project components
klaustopher Oct 26, 2026
123ef3c
Filter custom field projects through a project query
klaustopher Oct 26, 2026
77b94bd
Avoid N+1 queries on the custom field projects pages
klaustopher Oct 26, 2026
9d8222c
Check custom field usage of a project without loading all usages
klaustopher Oct 26, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
module Admin
module CustomFields
module CustomFieldProjects
class RowComponent < Projects::RowComponent
class RowComponent < ::Projects::RowComponent
include OpTurbo::Streamable

def wrapper_uniq_by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
module Admin
module CustomFields
module CustomFieldProjects
class TableComponent < Projects::TableComponent
class TableComponent < ::Projects::TableComponent
include ::Projects::Concerns::TableComponent::StreamablePaginationLinksConstraints

def columns
Expand Down
22 changes: 18 additions & 4 deletions app/components/admin/custom_fields/edit_form_header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ module CustomFields
class EditFormHeaderComponent < ApplicationComponent
include ::CustomFields::AdminRoutes

def initialize(custom_field:, selected:, section_label:, page_title:, **)
def initialize(custom_field:, selected:, section_label:, page_title:, project: nil, **)
@custom_field = custom_field
@selected = selected
@section_label = section_label
@page_title = page_title
@project = project
super(custom_field, **)
end

Expand All @@ -61,7 +62,8 @@ def items_tab
def extra_tabs
return [] unless @custom_field.is_a?(WorkPackageCustomField)

[{ name: "attribute_help_text", path: attribute_help_text_path(@custom_field),
[{ name: "projects", path: projects_path(@custom_field), label: t(:label_project_plural) },
{ name: "attribute_help_text", path: attribute_help_text_path(@custom_field),
label: AttributeHelpText.human_attribute_name(:help_text) }]
end

Expand All @@ -75,10 +77,22 @@ def breadcrumbs_items
{ href: admin_index_path, text: t(:label_administration) },
{ href: index_path(@custom_field), text: @section_label },
{ href: index_path(@custom_field), text: @page_title },
helpers.nested_breadcrumb_element(helpers.label_for_custom_field_format(model.field_format),
@custom_field.attribute_in_database("name"))
*custom_field_breadcrumbs_items
]
end

def custom_field_breadcrumbs_items
name = @custom_field.attribute_in_database("name")

if @project
[
{ href: projects_path(@custom_field), text: name },
helpers.nested_breadcrumb_element(Project.model_name.human, @project.name)
]
else
[helpers.nested_breadcrumb_element(helpers.label_for_custom_field_format(model.field_format), name)]
end
end
end
end
end
76 changes: 76 additions & 0 deletions app/components/admin/custom_fields/projects/row_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# 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 Admin
module CustomFields
module Projects
class RowComponent < OpPrimer::BorderBoxRowComponent
include ::CustomFields::AdminRoutes

alias_method :project, :model

def row_data = { test_selector: "custom-field-projects-row-#{project.id}" }

def name
render(::Projects::InfoLineComponent.new(project:))
end

def types
safe_join(table.types_of(project).map { |type| link_to(type.name, type_settings_path(type_id: type.id)) }, ", ")
end

def project_specific_values
I18n.t("custom_fields.admin.projects.enabled_values",
count: table.usable_value_counts.fetch(project.id),
total: table.value_count)
end

def button_links
[configure_button]
end

private

def configure_button
render(
Primer::Beta::IconButton.new(
icon: :"project-symlink",
tag: :a,
href: project_values_path(table.custom_field, project),
scheme: :default,
aria: { label: I18n.t("custom_fields.admin.projects.configure", project: project.name) },
tooltip_direction: :w
)
)
end
end
end
end
end
112 changes: 112 additions & 0 deletions app/components/admin/custom_fields/projects/table_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 Admin
module CustomFields
module Projects
class TableComponent < OpPrimer::BorderBoxTableComponent
FRAME_ID = "custom-field-projects-list"

columns :name, :types, :project_specific_values
main_column :name
mobile_labels :types, :project_specific_values

attr_reader :custom_field

def initialize(rows:, custom_field:, type_ids_by_project_id:, filtered: false)
super(rows:)

@custom_field = custom_field
@type_ids_by_project_id = type_ids_by_project_id
@filtered = filtered
end

def headers
[
[:name, { caption: Project.model_name.human }],
[:types, { caption: I18n.t("custom_fields.admin.projects.types_column") }],
if project_specific_values?
[:project_specific_values,
{ caption: CustomField.human_attribute_name(:project_specific_value_restriction) }]
end
].compact
end

def skip_column?(column)
column == :project_specific_values && !project_specific_values?
end

def project_specific_values?
custom_field.project_specific_value_restriction.present?
end

def types_of(project)
types_by_id.values_at(*@type_ids_by_project_id.fetch(project.id, [])).compact.sort_by(&:position)
end

def value_count
@value_count ||= custom_field.restrictable_values.count
end

def usable_value_counts
@usable_value_counts ||= custom_field.usable_value_counts_in(Project.where(id: rows.map(&:id)))
end

def has_actions? = project_specific_values?

def mobile_title = I18n.t(:label_project_plural)

def blank_title
if @filtered
I18n.t("custom_fields.admin.projects.filter_no_results.title")
else
I18n.t("custom_fields.admin.projects.empty_state.title")
end
end

def blank_description
if @filtered
I18n.t("custom_fields.admin.projects.filter_no_results.description")
else
I18n.t("custom_fields.admin.projects.empty_state.description")
end
end

def blank_icon = @filtered ? :search : :project

private

def types_by_id
@types_by_id ||= ::Type.where(id: @type_ids_by_project_id.values.flatten).index_by(&:id)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<%#-- 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, see <https://www.gnu.org/licenses/>.

See COPYRIGHT and LICENSE files for more details.

++#%>

<%= helpers.turbo_frame_tag("custom-field-project-values", **frame_arguments) do %>
<%=
render(Primer::Beta::BorderBox.new(test_selector: "custom-field-project-values", padding: :condensed)) do |box|
box.with_header do
flex_layout(justify_content: :space_between, align_items: :center) do |header|
header.with_column do
render(Primer::Beta::Text.new(font_weight: :bold)) { custom_field.name }
end

header.with_column do
safe_join(
[
render(
Primer::Beta::Button.new(
scheme: :invisible,
size: :small,
tag: :a,
href: enable_all_path,
data: { turbo_method: :post }
)
) do |button|
button.with_leading_visual_icon(icon: :"check-circle")
t("custom_fields.admin.projects.enable_all")
end,
render(
Primer::Beta::Button.new(
scheme: :invisible,
size: :small,
tag: :a,
href: disable_all_path,
data: { turbo_method: :post }
)
) do |button|
button.with_leading_visual_icon(icon: :"x-circle")
t("custom_fields.admin.projects.disable_all")
end
]
)
end
end
end

rows.each do |row|
box.with_row(test_selector: "custom-field-project-value-#{row.id}") do
flex_layout(justify_content: :space_between, align_items: :center) do |line|
line.with_column(pl: indentation(row)) do
safe_join(
[
render(Primer::Beta::Text.new) { row.label },
(render(Primer::Beta::Text.new(color: :muted, font_size: :small, ml: 2)) { row.caption } if row.caption)
]
)
end

line.with_column do
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_path(row),
csrf_token: helpers.form_authenticity_token,
checked: row.enabled,
size: :small,
aria: { label: row.label }
)
)
end
end
end
end
end
%>
<% end %>
Loading
Loading