diff --git a/app/components/admin/custom_fields/edit_form_header_component.rb b/app/components/admin/custom_fields/edit_form_header_component.rb index 85a0319df35..46c9ac4fd79 100644 --- a/app/components/admin/custom_fields/edit_form_header_component.rb +++ b/app/components/admin/custom_fields/edit_form_header_component.rb @@ -46,12 +46,18 @@ module Admin } ] - if @custom_field.hierarchical_list? || @custom_field.list? + if @custom_field.hierarchical_list? tabs << { name: "items", path: custom_field_items_path(@custom_field), label: t(:label_item_plural) } + elsif @custom_field.list? + tabs << { + name: "items", + path: list_items_custom_field_path(@custom_field), + label: t(:label_item_plural) + } end if @custom_field.is_a?(WorkPackageCustomField) || diff --git a/app/components/custom_fields/details_component.rb b/app/components/custom_fields/details_component.rb index 6d3e345a815..31b51f12b82 100644 --- a/app/components/custom_fields/details_component.rb +++ b/app/components/custom_fields/details_component.rb @@ -44,7 +44,11 @@ module CustomFields alias_method :custom_field, :model def form_url - model.new_record? ? custom_fields_path : custom_field_path(model) + if model.new_record? + model.type == "ProjectCustomField" ? admin_settings_project_custom_fields_path : custom_fields_path + else + model.type == "ProjectCustomField" ? admin_settings_project_custom_field_path(model) : custom_field_path(model) + end end def form_method @@ -65,7 +69,7 @@ module CustomFields def show_top_banner? case custom_field.field_format - when "hierarchy", "weighted_item_list" + when "hierarchy", "weighted_item_list", "list" persisted_cf_has_no_items_or_projects? else false @@ -74,12 +78,16 @@ module CustomFields def top_banner_text case custom_field.field_format - when "hierarchy", "weighted_item_list" + when "hierarchy", "weighted_item_list", "list" I18n.t("custom_fields.admin.notice.remember_items_and_projects") end end def persisted_cf_has_no_items_or_projects? + if custom_field.list? && custom_field.custom_options.empty? && custom_field.projects.empty? + return true + end + custom_field.persisted? && custom_field.hierarchical_list? && custom_field.hierarchy_root.children.empty? && diff --git a/app/components/settings/project_custom_fields/edit_form_header_component.rb b/app/components/settings/project_custom_fields/edit_form_header_component.rb index 4f52c6e58d1..3a2bda6d415 100644 --- a/app/components/settings/project_custom_fields/edit_form_header_component.rb +++ b/app/components/settings/project_custom_fields/edit_form_header_component.rb @@ -51,6 +51,12 @@ module Settings path: admin_settings_project_custom_field_items_path(@custom_field), label: t(:label_item_plural) } + elsif @custom_field.list? + tabs << { + name: "items", + path: list_items_admin_settings_project_custom_field_path(@custom_field), + label: t(:label_item_plural) + } end if @custom_field.user? diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index 4e5c6ac031d..75c3460e77e 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -43,7 +43,7 @@ module Admin::Settings before_action :find_custom_field, only: %i(show edit project_mappings new_link link unlink update destroy delete_option reorder_alphabetical move drop role_assignment update_role_assignment role_assignment_preview_dialog - attribute_help_text update_attribute_help_text) + attribute_help_text update_attribute_help_text list_items) before_action :prepare_custom_option_position, only: %i(update create) before_action :find_custom_option, only: :delete_option before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink] @@ -74,6 +74,8 @@ module Admin::Settings def edit; end + def list_items; end + def project_mappings; end def role_assignment; end diff --git a/app/controllers/concerns/custom_fields/shared_actions.rb b/app/controllers/concerns/custom_fields/shared_actions.rb index 625a1e7e369..8cd591d82a4 100644 --- a/app/controllers/concerns/custom_fields/shared_actions.rb +++ b/app/controllers/concerns/custom_fields/shared_actions.rb @@ -49,6 +49,14 @@ module CustomFields end end + def list_item_path(custom_field, params = {}) + if custom_field.type == "ProjectCustomField" + list_items_admin_settings_project_custom_field_path(**params) + else + list_items_custom_field_path(**params) + end + end + def create # rubocop:disable Metrics/AbcSize call = ::CustomFields::CreateService .new(user: current_user) @@ -66,10 +74,14 @@ module CustomFields end def update - perform_update(get_custom_field_params) + if custom_options_attributes + perform_update(get_custom_field_params, tab: :list_items) + else + perform_update(get_custom_field_params) + end end - def perform_update(custom_field_params) + def perform_update(custom_field_params, tab: :edit) call = ::CustomFields::UpdateService .new(user: current_user, model: @custom_field) .call(custom_field_params) @@ -77,7 +89,13 @@ module CustomFields if call.success? flash[:notice] = t(:notice_successful_update) call_hook(:controller_custom_fields_edit_after_save, custom_field: @custom_field) - redirect_back_or_default(edit_path(@custom_field, id: @custom_field.id)) + path = if tab == :list_items + list_item_path(@custom_field, + id: @custom_field.id) + else + edit_path(@custom_field, id: @custom_field.id) + end + redirect_to(path) else render action: :edit, status: :unprocessable_entity end @@ -89,10 +107,10 @@ module CustomFields .sort_by(&:value) .each_with_index .map do |custom_option, index| - { id: custom_option.id, position: index + 1 } + { id: custom_option.id, position: index + 1 } end - perform_update(custom_options_attributes: reordered_options) + perform_update({ custom_options_attributes: reordered_options }, tab: :list_items) end def destroy @@ -115,7 +133,7 @@ module CustomFields flash[:error] = @custom_option.errors.full_messages end - redirect_to edit_path(@custom_field, id: @custom_field.id), status: :see_other + redirect_to list_item_path(@custom_field, id: @custom_field.id), status: :see_other end def new_custom_field @@ -139,14 +157,18 @@ module CustomFields end def prepare_custom_option_position - return unless params[:custom_field][:custom_options_attributes] + return unless custom_options_attributes index = 0 - params[:custom_field][:custom_options_attributes].each_value do |attributes| + custom_options_attributes.each_value do |attributes| attributes[:position] = (index = index + 1) end end + + def custom_options_attributes + params[:custom_field][:custom_options_attributes] + end end end end diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 37180bfc507..70bc7741c7d 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -31,11 +31,14 @@ class CustomFieldsController < ApplicationController include CustomFields::SharedActions # share logic with ProjectCustomFieldsControlller include CustomFields::AttributeHelpTextActions + layout "admin" # rubocop:disable Rails/LexicallyScopedActionFilter before_action :require_admin - before_action :find_custom_field, only: %i(edit update destroy delete_option reorder_alphabetical attribute_help_text update_attribute_help_text) + before_action :find_custom_field, + only: %i(edit update destroy delete_option reorder_alphabetical attribute_help_text update_attribute_help_text + list_items) before_action :prepare_custom_option_position, only: %i(update create) before_action :find_custom_option, only: :delete_option before_action :validate_enterprise_token, only: %i(create) @@ -67,6 +70,8 @@ class CustomFieldsController < ApplicationController render_attribute_help_text_form end + def list_items; end + def update_attribute_help_text update_help_text end diff --git a/app/forms/custom_fields/details_form.rb b/app/forms/custom_fields/details_form.rb index 4eaf8771b8b..57d4f747f26 100644 --- a/app/forms/custom_fields/details_form.rb +++ b/app/forms/custom_fields/details_form.rb @@ -61,23 +61,21 @@ module CustomFields end if show_min_max_field? - details_form.group(layout: :horizontal) do |g| - g.text_field( - name: :min_length, - type: :number, - label: label(:min_length), - caption: instructions(:min_max), - input_width: :xsmall - ) + details_form.text_field( + name: :min_length, + type: :number, + label: label(:min_length), + caption: instructions(:min_max), + input_width: :small + ) - g.text_field( - name: :max_length, - type: :number, - label: label(:max_length), - caption: instructions(:min_max), - input_width: :xsmall - ) - end + details_form.text_field( + name: :max_length, + type: :number, + label: label(:max_length), + caption: instructions(:min_max), + input_width: :small + ) end if show_regex_field? @@ -220,7 +218,7 @@ module CustomFields end def show_default_text_field? - %w[list bool date text user version hierarchy calculated_value].exclude?(model.field_format) + %w[list bool date text user version hierarchy weighted_item_list calculated_value].exclude?(model.field_format) end def show_default_rich_text_field? @@ -232,11 +230,11 @@ module CustomFields end def show_min_max_field? - %w[list bool date user version link hierarchy calculated_value].exclude?(model.field_format) + %w[list bool date user version link hierarchy weighted_item_list calculated_value].exclude?(model.field_format) end def show_regex_field? - %w[list bool date user version hierarchy calculated_value].exclude?(model.field_format) + %w[list bool date user version hierarchy weighted_item_list calculated_value].exclude?(model.field_format) end def show_right_to_left_field? @@ -260,7 +258,7 @@ module CustomFields end def show_is_searchable_field? - %w[bool date float int user version hierarchy calculated_value].exclude?(model.field_format) + %w[bool date float int user version hierarchy weighted_item_list calculated_value].exclude?(model.field_format) end def show_non_open_versions_field? diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 55b4d5b62e7..9ed29fbb74c 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -61,9 +61,6 @@ class CustomField < ApplicationRecord acts_as_list scope: [:type] validates :field_format, presence: true - validates :custom_options, - presence: { message: ->(*) { I18n.t(:"activerecord.errors.models.custom_field.at_least_one_custom_option") } }, - if: ->(*) { field_format == "list" } validates :name, presence: true, length: { maximum: 256 }, diff --git a/app/views/admin/settings/project_custom_fields/list_items.html.erb b/app/views/admin/settings/project_custom_fields/list_items.html.erb new file mode 100644 index 00000000000..f0adfc60203 --- /dev/null +++ b/app/views/admin/settings/project_custom_fields/list_items.html.erb @@ -0,0 +1,76 @@ +<%#-- 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. + +++#%> + +<% html_title t(:label_administration), t("settings.project_attributes.heading"), @custom_field.name %> + +<%= + render( + Settings::ProjectCustomFields::EditFormHeaderComponent.new( + custom_field: @custom_field, + selected: :project_custom_field_edit + ) + ) +%> + +<%= settings_primer_form_with( + model: @custom_field, + scope: :custom_field, + url: admin_settings_project_custom_field_path(@custom_field), + html: { method: :put, id: "custom_field_form" } + ) do |f| %> + <%= render partial: "custom_fields/custom_options", locals: { custom_field: @custom_field, f: f } %> + + <%= + flex_layout(mt: 4) do |flex| + flex.with_column do + render Primer::Beta::Button.new( + scheme: :secondary, + tag: :a, + href: reorder_alphabetical_admin_settings_project_custom_field_path(@custom_field), + data: { + turbo_method: :post, + turbo_confirm: t("custom_fields.reorder_confirmation") + }, + mr: 2 + ) do + t("custom_fields.reorder_alphabetical") + end + end + flex.with_column do + render Primer::Beta::Button.new( + scheme: :primary, + type: :submit, + ) do |button| + button.with_leading_visual_icon(icon: :check) + t(:button_submit) + end + end + end + %> +<% end %> diff --git a/app/views/custom_fields/_custom_options.html.erb b/app/views/custom_fields/_custom_options.html.erb new file mode 100644 index 00000000000..b122d871e5a --- /dev/null +++ b/app/views/custom_fields/_custom_options.html.erb @@ -0,0 +1,170 @@ +<%#-- 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. + +++#%> +<% content_controller "admin--custom-fields", + "admin--custom-fields-multi-select-value": @custom_field.multi_value? %> + +<% custom_field.custom_options.build if custom_field.custom_options.empty? %> + +
|
+
+
+
+
+ <%= t("activerecord.attributes.custom_value.value") %>
+
+
+ |
+
+
+
+
+
+ <%= t(:label_default) %>
+
+
+ |
+
+
+
+
+
+ <%= t(:button_sort) %>
+
+
+ |
+
+
+
+ |
+
|---|---|---|---|
| + + <%= co_f.hidden_field :id, + class: "custom-option-id" %> + + <%= co_f.text_field :value, + no_label: true %> + + | ++ <%= co_f.check_box :default_value, + container_class: "custom-option-default-value", + data: { + "admin--custom-fields-target": "customOptionDefaults", + action: "click->admin--custom-fields#uncheckOtherDefaults" + }, + no_label: true %> + | ++ + | ++ <%= link_to "", + delete_option_of_custom_field_path(id: custom_field.id || 0, option_id: custom_option.id || 0), + data: { + turbo_method: :delete, + action: "admin--custom-fields#removeOption", + turbo_confirm: t(:"custom_fields.confirm_destroy_option") + }, + class: "icon icon-delete delete-custom-option", + title: t(:button_delete) %> + | +