From 4e80bd30640b61729a313d00b95f50cafed9f38f Mon Sep 17 00:00:00 2001 From: Andreas Pfohl Date: Thu, 26 Sep 2024 14:42:13 +0200 Subject: [PATCH] [57812] Added model and migration for custom field of type hierarchy (#16805) * Added model and migration for custom field of type hierarchy * Added feature flag for custom field of type hierarchy * moved feature flag to drop down options --------- Co-authored-by: Eric Schubert --- app/helpers/custom_fields_helper.rb | 5 ++ app/models/custom_field.rb | 5 ++ app/models/custom_field/hierarchy/item.rb | 36 +++++++++++++ app/models/custom_value/hierarchy_strategy.rb | 52 +++++++++++++++++++ config/initializers/custom_field_format.rb | 11 ++-- config/initializers/feature_decisions.rb | 3 ++ config/locales/en.yml | 1 + ...0240924114246_create_hierarchical_items.rb | 29 +++++++++++ lib/api/v3/utilities/custom_field_injector.rb | 11 ++-- 9 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 app/models/custom_field/hierarchy/item.rb create mode 100644 app/models/custom_value/hierarchy_strategy.rb create mode 100644 db/migrate/20240924114246_create_hierarchical_items.rb diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index e5c8951c2e2d..229e80f835aa 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -194,10 +194,15 @@ def format_value(value, custom_field) # Return an array of custom field formats which can be used in select_tag def custom_field_formats_for_select(custom_field) + hierarchy_if_deactivated = lambda do |format| + format.name == "hierarchy" && !OpenProject::FeatureDecisions.custom_field_of_type_hierarchy_active? + end + OpenProject::CustomFieldFormat .all_for_field(custom_field) .sort_by(&:order) .reject { |format| format.label.nil? } + .reject(&hierarchy_if_deactivated) .map do |custom_field_format| [label_for_custom_field_format(custom_field_format.name), custom_field_format.name] end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 076d972709d4..873b78ff3569 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -41,6 +41,11 @@ class CustomField < ApplicationRecord inverse_of: "custom_field" accepts_nested_attributes_for :custom_options + has_one :hierarchy_root, + class_name: "CustomField::Hierarchy::Item", + dependent: :delete, # todo: cascade into children with service + inverse_of: "custom_field" + acts_as_list scope: [:type] validates :field_format, presence: true diff --git a/app/models/custom_field/hierarchy/item.rb b/app/models/custom_field/hierarchy/item.rb new file mode 100644 index 000000000000..3ae4d4ff52a7 --- /dev/null +++ b/app/models/custom_field/hierarchy/item.rb @@ -0,0 +1,36 @@ +# 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 CustomField::Hierarchy::Item < ApplicationRecord + self.table_name = "hierarchical_items" + + belongs_to :custom_field + has_closure_tree order: "sort_order", numeric_order: true +end diff --git a/app/models/custom_value/hierarchy_strategy.rb b/app/models/custom_value/hierarchy_strategy.rb new file mode 100644 index 000000000000..e87133e0fab1 --- /dev/null +++ b/app/models/custom_value/hierarchy_strategy.rb @@ -0,0 +1,52 @@ +#-- 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 CustomValue::HierarchyStrategy < CustomValue::ARObjectStrategy + def validate_type_of_value + raise NotImplementedError + end + + def typed_value + raise NotImplementedError + end + + private + + def ar_class + CustomField::Hierarchy::Item + end + + def ar_object(value) + option = CustomField::Hierarchy::Item.find_by(id: value.to_s) + if option.nil? + "#{value} #{I18n.t(:label_not_found)}" + else + option.value + end + end +end diff --git a/config/initializers/custom_field_format.rb b/config/initializers/custom_field_format.rb index f99a0402b4bc..9784807381dd 100644 --- a/config/initializers/custom_field_format.rb +++ b/config/initializers/custom_field_format.rb @@ -61,15 +61,13 @@ formatter: "CustomValue::BoolStrategy") fields.register OpenProject::CustomFieldFormat.new("user", label: Proc.new { User.model_name.human }, - only: %w(WorkPackage TimeEntry - Version Project), + only: %w(WorkPackage TimeEntry Version Project), edit_as: "list", order: 9, formatter: "CustomValue::UserStrategy") fields.register OpenProject::CustomFieldFormat.new("version", label: Proc.new { Version.model_name.human }, - only: %w(WorkPackage TimeEntry - Version Project), + only: %w(WorkPackage TimeEntry Version Project), edit_as: "list", order: 10, formatter: "CustomValue::VersionStrategy") @@ -79,4 +77,9 @@ label: nil, order: 11, formatter: "CustomValue::EmptyStrategy") + + fields.register OpenProject::CustomFieldFormat.new("hierarchy", + label: :label_hierarchy, + order: 12, + formatter: "CustomValue::HierarchyStrategy") end diff --git a/config/initializers/feature_decisions.rb b/config/initializers/feature_decisions.rb index d2d1d2caeb6e..2b8ee714164b 100644 --- a/config/initializers/feature_decisions.rb +++ b/config/initializers/feature_decisions.rb @@ -40,3 +40,6 @@ # end OpenProject::FeatureDecisions.add :built_in_oauth_applications, description: "Allows the display and use of built-in OAuth applications." + +OpenProject::FeatureDecisions.add :custom_field_of_type_hierarchy, + description: "Allows the use of the custom field type 'Hierarchy'." diff --git a/config/locales/en.yml b/config/locales/en.yml index b2503096c448..14e5ba26bd8b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2268,6 +2268,7 @@ en: label_here: here label_hide: "Hide" label_history: "History" + label_hierarchy: "Hierarchy" label_hierarchy_leaf: "Hierarchy leaf" label_home: "Home" label_subject_or_id: "Subject or ID" diff --git a/db/migrate/20240924114246_create_hierarchical_items.rb b/db/migrate/20240924114246_create_hierarchical_items.rb new file mode 100644 index 000000000000..deddfcd0c406 --- /dev/null +++ b/db/migrate/20240924114246_create_hierarchical_items.rb @@ -0,0 +1,29 @@ +class CreateHierarchicalItems < ActiveRecord::Migration[7.1] + def change + create_table :hierarchical_items do |t| + t.integer :parent_id + t.integer :sort_order + t.string :label + t.string :short + t.boolean :is_deleted, default: false, null: false + + t.timestamps + end + + add_reference "hierarchical_items", :custom_field, foreign_key: true + + # auto-generated by closure_tree + create_table :hierarchical_item_hierarchies, id: false do |t| + t.integer :ancestor_id, null: false + t.integer :descendant_id, null: false + t.integer :generations, null: false + end + + add_index :hierarchical_item_hierarchies, %i[ancestor_id descendant_id generations], + unique: true, + name: "item_anc_desc_idx" + + add_index :hierarchical_item_hierarchies, [:descendant_id], + name: "item_desc_idx" + end +end diff --git a/lib/api/v3/utilities/custom_field_injector.rb b/lib/api/v3/utilities/custom_field_injector.rb index 84a71ddc146e..8cb34ef32e45 100644 --- a/lib/api/v3/utilities/custom_field_injector.rb +++ b/lib/api/v3/utilities/custom_field_injector.rb @@ -41,21 +41,24 @@ class CustomFieldInjector "bool" => "Boolean", "user" => "User", "version" => "Version", - "list" => "CustomOption" + "list" => "CustomOption", + "hierarchy" => "CustomField::Hierarchy::Item" }.freeze - LINK_FORMATS = %w(list user version).freeze + LINK_FORMATS = %w(list user version hierarchy).freeze NAMESPACE_MAP = { "user" => ["users", "groups", "placeholder_users"], "version" => "versions", - "list" => "custom_options" + "list" => "custom_options", + "hierarchy" => "hierarchical_items" }.freeze REPRESENTER_MAP = { "user" => "::API::V3::Principals::PrincipalRepresenterFactory", "version" => "::API::V3::Versions::VersionRepresenter", - "list" => "::API::V3::CustomOptions::CustomOptionRepresenter" + "list" => "::API::V3::CustomOptions::CustomOptionRepresenter", + "hierarchy" => "::API::V3::HierarchyItems::HierarchyItemRepresenter" }.freeze class << self