Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0dfb625
Organize user custom fields within sections. wp/72005
dfriquet May 28, 2026
b58934c
Fix a modal dialog closing before the error could be rendered.
dfriquet May 28, 2026
e9376ca
Display user custom fields grouped by section on profile and edit for…
dfriquet May 29, 2026
60032f0
Show user custom fields on the hover card when flagged. wp/72005
dfriquet Jun 1, 2026
e55fa08
Fix linting errors. wp/72005
dfriquet Jun 1, 2026
dcad63a
Allow hierarchy custom fields for User. wp/72005
dfriquet Jun 1, 2026
55d3376
Reduce DB round-trips. wp/72005
dfriquet Jun 2, 2026
6907e1a
Include built-in fields in User attributes sections. wp/72005
dfriquet Jun 4, 2026
f7eb104
Refactor Projects custom field ordering to match Users one. wp/72005
dfriquet Jun 5, 2026
f9ed9a4
DRY up code between Users and Projects section management. wp/72005
dfriquet Jun 5, 2026
56892be
Fix yamllint errors.
dfriquet Jun 8, 2026
b02e4fc
Add 'New attribute' submenu to project section actions. wp/72005
dfriquet Jun 9, 2026
5dc6815
Add 'New attribute' submenu to user section actions. wp/72005
dfriquet Jun 9, 2026
52e788b
Add a quick filter search input for user attributes. wp/72005
dfriquet Jun 9, 2026
5fa782a
Namespace user attributes i18n keys to match component code. wp/72005
dfriquet Jun 9, 2026
1afb28c
Remove user custom fields from the legacy custom fields admin. wp/72005
dfriquet Jun 12, 2026
4972fce
Improve UI/UX for user custom fields admin and profile. wp/72005
dfriquet Jun 12, 2026
491bf34
Show built-in attributes on user profile page. wp/72005
dfriquet Jun 12, 2026
211e72c
Send back flash messages on section action error. wp/72005
dfriquet Jun 12, 2026
87f1f83
Merge branch 'dev' into feature/72005-new-administration-for-user-cus…
klaustopher Jun 23, 2026
76c3819
Merge branch 'dev' into feature/72005-new-administration-for-user-cus…
klaustopher Jun 23, 2026
415626b
Remove usage of now removed feature flag
klaustopher Jun 23, 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
12 changes: 10 additions & 2 deletions app/components/custom_fields/details_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ class DetailsComponent < ApplicationComponent

def form_url
if model.new_record?
model.type == "ProjectCustomField" ? admin_settings_project_custom_fields_path : custom_fields_path
case model.type
when "ProjectCustomField" then admin_settings_project_custom_fields_path
when "UserCustomField" then admin_settings_user_custom_fields_path
else custom_fields_path
end
else
model.type == "ProjectCustomField" ? admin_settings_project_custom_field_path(model) : custom_field_path(model)
case model.type
when "ProjectCustomField" then admin_settings_project_custom_field_path(model)
when "UserCustomField" then admin_settings_user_custom_field_path(model)
else custom_field_path(model)
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ def initialize(project:, project_custom_field_section:)

@project = project
@project_custom_field_section = project_custom_field_section
enabled_custom_field_ids = project.enabled_custom_field_ids
@project_custom_fields = project_custom_field_section.custom_fields.where(id: enabled_custom_field_ids)
enabled_ids = project.enabled_custom_field_ids.to_set
@project_custom_fields = project_custom_field_section.custom_fields_in_order
.select { |cf| enabled_ids.include?(cf.id) }
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def initialize(project:, project_custom_field_section:)

@project = project
@project_custom_field_section = project_custom_field_section
@project_custom_fields = project_custom_field_section.custom_fields
@project_custom_fields = project_custom_field_section.custom_fields_in_order
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ class CustomFieldRowComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

def initialize(project_custom_field:, first_and_last:)
super
def initialize(project_custom_field:, first:, last:)
super()

@project_custom_field = project_custom_field
@first_and_last = first_and_last
@first = first
@last = last
end

private
Expand Down Expand Up @@ -101,21 +102,11 @@ def delete_action_item(menu)
end

def first?
@first ||=
if @first_and_last.first
@first_and_last.first == @project_custom_field
else
@project_custom_field.first?
end
@first
end

def last?
@last ||=
if @first_and_last.last
@first_and_last.last == @project_custom_field
else
@project_custom_field.last?
end
@last
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ def form_config
{
model: @project_custom_field_section,
method: @project_custom_field_section.persisted? ? :put : :post,
url: @project_custom_field_section.persisted? ? admin_settings_project_custom_field_section_path(@project_custom_field_section) : admin_settings_project_custom_field_sections_path
url: if @project_custom_field_section.persisted?
admin_settings_project_custom_field_section_path(@project_custom_field_section)
else
admin_settings_project_custom_field_sections_path
end,
data: { turbo_stream: true }
}
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
Primer::Alpha::Dialog.new(
title: t("settings.project_attributes.label_new_section"),
size: :medium_portrait,
id: MODAL_ID
id: MODAL_ID,
data: { "keep-open-on-submit": true }
)
) do %>
<%= render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new) %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,37 @@
menu.with_show_button(icon: "kebab-horizontal", "aria-label": t("settings.project_attributes.label_section_actions"), scheme: :invisible)
edit_action_item(menu)
move_actions(menu)
if @project_custom_fields.empty?
if @ordered_cfs.empty?
delete_action_item(menu)
else
disabled_delete_action_item(menu)
end

menu.with_divider

menu.with_sub_menu_item(
label: t("settings.project_attributes.label_new_attribute"),
content_arguments: {
aria: { label: t("settings.project_attributes.label_add_attribute") },
data: { test_selector: "new-project-custom-field-in-section-submenu" }
}
) do |sub_menu|
sub_menu.with_leading_visual_icon(icon: :plus)
OpenProject::CustomFieldFormat.available_for_class_name("Project")
.sort_by(&:name)
.each do |format|
action_menu_item_for_custom_field_format(sub_menu, format)
end
end
end
end

actions_container.with_column do
render(
Primer::Alpha::Dialog.new(
id: "project-custom-field-section-dialog#{@project_custom_field_section.id}", title: t("settings.project_attributes.label_new_section"),
size: :medium_portrait
size: :medium_portrait,
data: { "keep-open-on-submit": true }
)
) do |_dialog|
render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new(project_custom_field_section: @project_custom_field_section))
Expand All @@ -73,7 +91,7 @@
end
end
end
if @project_custom_fields.empty?
if @ordered_cfs.empty?
component.with_row(data: { "empty-list-item": true }) do
flex_layout(align_items: :center, justify_content: :space_between) do |empty_list_container|
empty_list_container.with_column(ml: 4, mr: 2) do
Expand All @@ -93,23 +111,21 @@

OpenProject::CustomFieldFormat.available_for_class_name("Project")
.sort_by(&:name)
.map do |format|
action_menu_item_for_custom_field_format(menu, format)
.each do |format|
action_menu_item_for_custom_field_format(menu, format)
end
end
end
end
end
else
first_and_last = [@project_custom_fields.first, @project_custom_fields.last]
@project_custom_fields.each do |project_custom_field|
component.with_row(
data: draggable_item_config(project_custom_field)
) do
@ordered_cfs.each_with_index do |cf, index|
component.with_row(data: draggable_item_config(cf)) do
render(
custom_field_row_component_class.new(
project_custom_field:,
first_and_last:
project_custom_field: cf,
first: index == 0,
last: index == @ordered_cfs.size - 1
)
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def initialize(project_custom_field_section:, first_and_last: [])
super

@project_custom_field_section = project_custom_field_section
@project_custom_fields = project_custom_field_section.custom_fields
@ordered_cfs = project_custom_field_section.custom_fields_in_order

@first_and_last = first_and_last
end
Expand All @@ -57,9 +57,9 @@ def wrapper_uniq_by
def drag_and_drop_target_config
{
generic_drag_and_drop_target: "container",
"target-container-accessor": ".Box > ul", # the accessor of the container that contains the drag and drop items
"target-id": @project_custom_field_section.id, # the id of the target
"target-allowed-drag-type": "custom-field" # the type of dragged items which are allowed to be dropped in this target
"target-container-accessor": ".Box > ul",
"target-id": @project_custom_field_section.id,
"target-allowed-drag-type": "custom-field"
}
end

Expand Down Expand Up @@ -132,21 +132,23 @@ def delete_action_item(menu)
end

def first?
@first ||=
if @first_and_last.first
@first_and_last.first == @project_custom_field_section
else
@project_custom_field_section.first?
end
return @first unless @first.nil?

@first = if @first_and_last.first
@first_and_last.first == @project_custom_field_section
else
@project_custom_field_section.first?
end
end

def last?
@last ||=
if @first_and_last.last
@first_and_last.last == @project_custom_field_section
else
@project_custom_field_section.last?
end
return @last unless @last.nil?

@last = if @first_and_last.last
@first_and_last.last == @project_custom_field_section
else
@project_custom_field_section.last?
end
end

def action_menu_item_for_custom_field_format(menu, format)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<%=
component_wrapper(
class: "op-user-built-in-attribute-container",
data: { test_selector: "user-built-in-attribute-container-#{@attribute_key}" }
) do
flex_layout(justify_content: :space_between, align_items: :center) do |main_container|
main_container.with_column(flex_layout: true, align_items: :center) do |content_container|
content_container.with_column(mr: 2) do
render(Primer::OpenProject::DragHandle.new(classes: "handle"))
end
content_container.with_column(mr: 2) do
render(Primer::Beta::Text.new(font_weight: :bold)) { attribute_label }
end
content_container.with_column do
render(Primer::Beta::Label.new(scheme: :secondary, size: :medium)) do
t(".label_built_in")
end
end
end
main_container.with_column do
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "built-in-attribute-action-menu" })) do |menu|
menu.with_show_button(
icon: "kebab-horizontal",
"aria-label": t(".label_built_in_attribute_actions"),
scheme: :invisible
)
move_actions(menu)
end
end
end
end
%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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 Settings
module UserCustomFieldSections
class BuiltInAttributeRowComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

def initialize(section:, attribute_key:, first:, last:)
super()
@section = section
@attribute_key = attribute_key
@first = first
@last = last
end

def attribute_label
User.human_attribute_name(@attribute_key)
end

private

def move_actions(menu)
unless @first
move_action_item(menu, :highest, t("label_agenda_item_move_to_top"), "move-to-top")
move_action_item(menu, :higher, t("label_agenda_item_move_up"), "chevron-up")
end
unless @last
move_action_item(menu, :lower, t("label_agenda_item_move_down"), "chevron-down")
move_action_item(menu, :lowest, t("label_agenda_item_move_to_bottom"), "move-to-bottom")
end
end

def move_action_item(menu, move_to, label_text, icon)
menu.with_item(
label: label_text,
href: move_admin_settings_user_custom_field_section_built_in_attribute_path(
@section, @attribute_key, move_to:
),
form_arguments: {
method: :put,
data: { "turbo-stream": true, test_selector: "built-in-attribute-move-#{move_to}" }
}
) do |item|
item.with_leading_visual_icon(icon:)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<%=
component_wrapper(class: "op-user-custom-field-container", data: { test_selector: "user-custom-field-container-#{@user_custom_field.id}" }) do
flex_layout(justify_content: :space_between, align_items: :center) do |main_container|
main_container.with_column(flex_layout: true, align_items: :center) do |content_container|
content_container.with_column(mr: 2) do
render(Primer::OpenProject::DragHandle.new(classes: "handle"))
end
content_container.with_column(mr: 2) do
render(
Primer::Beta::Link.new(
href: edit_admin_settings_user_custom_field_path(@user_custom_field),
underline: false,
font_weight: :bold,
data: { turbo: "false" }
)
) do
@user_custom_field.name
end
end
content_container.with_column(mr: 2) do
render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do
helpers.label_for_custom_field_format(@user_custom_field.field_format)
end
end
if @user_custom_field.required?
content_container.with_column do
render(Primer::Beta::Label.new(scheme: :attention, size: :medium)) do
UserCustomField.human_attribute_name(:required)
end
end
end
end
main_container.with_column do
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "user-custom-field-action-menu" })) do |menu|
menu.with_show_button(icon: "kebab-horizontal", "aria-label": t(".label_user_custom_field_actions"), scheme: :invisible)
edit_action_item(menu)
move_actions(menu)
delete_action_item(menu)
end
end
end
end
%>
Loading
Loading