only display hierarchy option in dropdown if ee permits

This commit is contained in:
ulferts
2025-06-18 11:25:30 +02:00
parent 1cd27a5eb8
commit d6caec6c55
23 changed files with 179 additions and 63 deletions
@@ -29,7 +29,7 @@
)
menu.with_sub_menu_item(label: t("settings.project_attributes.label_new_attribute")) do |sub_menu|
OpenProject::CustomFieldFormat.all_for_class_name("Project")
OpenProject::CustomFieldFormat.available_for_class_name("Project")
.sort_by(&:name)
.map do |format|
sub_menu.with_item(
+1 -14
View File
@@ -133,24 +133,11 @@ module CustomFieldsHelper
CustomValue.new(custom_field:, value:).formatted_value
end
# Return an array of custom field formats which can be used in select_tag
def custom_field_formats_for_select(custom_field)
OpenProject::CustomFieldFormat.all_for_field(custom_field)
.map do |custom_field_format|
[label_for_custom_field_format(custom_field_format.name), custom_field_format.name]
end
end
def label_for_custom_field_format(format_string)
format = OpenProject::CustomFieldFormat.find_by(name: format_string)
return "" if format.nil?
label = format.label.is_a?(Proc) ? format.label.call : I18n.t(format.label)
show_enterprise_text = format_string == "hierarchy" && !EnterpriseToken.allows_to?(:custom_field_hierarchies)
suffix = show_enterprise_text ? " (#{I18n.t(:"ee.upsell.title")})" : ""
"#{label}#{suffix}"
format.label.is_a?(Proc) ? format.label.call : I18n.t(format.label)
end
def options_for_list(custom_field, project)
+1 -1
View File
@@ -43,7 +43,7 @@ See COPYRIGHT and LICENSE files for more details.
aria: { label: I18n.t(:label_custom_field_new) }
}
) do |menu|
OpenProject::CustomFieldFormat.all_for_class_name(@tab.delete_suffix("CustomField"))
OpenProject::CustomFieldFormat.available_for_class_name(@tab.delete_suffix("CustomField"))
.sort_by(&:name)
.map do |format|
menu.with_item(
@@ -86,5 +86,6 @@ OpenProject::CustomFieldFormat.map do |fields|
only: %w(WorkPackage),
order: 12,
multi_value_possible: true,
enterprise_feature: :custom_field_hierarchies,
formatter: "CustomValue::HierarchyStrategy")
end
+13 -12
View File
@@ -30,10 +30,9 @@ module OpenProject
class CustomFieldFormat
include Redmine::I18n
cattr_reader :available
@@available = {}
class_attribute :registered, default: {}
attr_reader :name, :order, :label, :edit_as, :class_names
attr_reader :name, :order, :label, :edit_as, :class_names, :enterprise_feature
def initialize(name,
label:,
@@ -41,6 +40,7 @@ module OpenProject
edit_as: name,
only: nil,
multi_value_possible: false,
enterprise_feature: nil,
formatter: "CustomValue::StringStrategy")
@name = name
@label = label
@@ -48,6 +48,7 @@ module OpenProject
@edit_as = edit_as
@class_names = only
@multi_value_possible = multi_value_possible
@enterprise_feature = enterprise_feature
@formatter = formatter
end
@@ -67,23 +68,23 @@ module OpenProject
# Registers a custom field format
def register(custom_field_format, _options = {})
@@available[custom_field_format.name] = custom_field_format unless @@available.include?(custom_field_format.name)
registered[custom_field_format.name] = custom_field_format unless registered.include?(custom_field_format.name)
end
def available
registered
.select { |_, format| !format.enterprise_feature || EnterpriseToken.allows_to?(format.enterprise_feature) }
end
def available_formats
@@available.keys
available.keys
end
def find_by(name:)
@@available[name.to_s]
registered[name.to_s]
end
def all_for_field(custom_field)
class_name = custom_field.class.customized_class.name
all_for_class_name(class_name)
end
def all_for_class_name(class_name)
def available_for_class_name(class_name)
available
.values
.select { |field| field.class_names.nil? || field.class_names.include?(class_name) }
@@ -30,12 +30,12 @@
require "rails_helper"
RSpec.describe CustomFields::Hierarchy::GenerateRootContract do
RSpec.describe CustomFields::Hierarchy::GenerateRootContract, with_ee: [:custom_field_hierarchies] do
subject { described_class.new }
describe "#call" do
context "when hierarchy_root is nil" do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
it "is valid" do
result = subject.call(custom_field:)
@@ -45,7 +45,7 @@ RSpec.describe CustomFields::Hierarchy::GenerateRootContract do
context "when hierarchy_root is not nil" do
let(:hierarchy_root) { create(:hierarchy_item) }
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root:) }
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root:) }
it "is invalid" do
result = subject.call(custom_field:)
@@ -55,7 +55,7 @@ RSpec.describe CustomFields::Hierarchy::GenerateRootContract do
end
context "when inputs are valid" do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
it "creates a success result" do
expect(subject.call(custom_field:)).to be_success
@@ -31,7 +31,7 @@
require "spec_helper"
RSpec.describe Admin::CustomFields::Hierarchy::ItemsController do
RSpec.describe Admin::CustomFields::Hierarchy::ItemsController, with_ee: [:custom_field_hierarchies] do
let(:user) { create(:admin) }
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
@@ -107,20 +107,23 @@ RSpec.shared_examples_for "hierarchy custom fields on index page" do |type|
end
context "with an active enterprise token with custom_field_hierarchies feature", with_ee: [:custom_field_hierarchies] do
it "does not show the enterprise upsell banner" do
it "does not show the enterprise upsell banner and has the 'Hierarchy' option for creation" do
expect(page).to have_no_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
cf_page.expect_having_create_item "Hierarchy"
end
end
context "with an active enterprise token without custom_field_hierarchies feature", with_ee: [:another_feature] do
it "shows the enterprise upsell banner" do
it "shows the enterprise upsell banner and lacks the 'Hierarchy' option for creation" do
expect(page).to have_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
cf_page.expect_not_having_create_item "Hierarchy"
end
end
context "with a trial enterprise token", :with_ee_trial, with_ee: [:custom_field_hierarchies] do
it "shows the enterprise upsell banner and can save" do
it "shows the enterprise upsell banner and has the 'Hierarchy' option for creation" do
expect(page).to have_text(I18n.t("ee.upsell.custom_field_hierarchies.description"))
cf_page.expect_having_create_item "Hierarchy"
end
end
end
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "Work package filtering by hierarchy custom field", :js do
RSpec.describe "Work package filtering by hierarchy custom field", :js, with_ee: [:custom_field_hierarchies] do
let(:project) { create(:project) }
let(:type) { project.types.first }
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe API::V3::CustomFields::Hierarchy::HierarchyItemRepresenter, "rendering" do
RSpec.describe API::V3::CustomFields::Hierarchy::HierarchyItemRepresenter, "rendering", with_ee: [:custom_field_hierarchies] do
include API::V3::Utilities::PathHelper
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
@@ -0,0 +1,107 @@
# 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.
# ++
require "spec_helper"
RSpec.describe OpenProject::CustomFieldFormat do
describe ".available_for_class_name" do
shared_examples_for "custom field formats" do |class_name, expected_formats|
it "returns all custom field formats for the '#{class_name}' class", :aggregate_failures do
formats = described_class.available_for_class_name(class_name)
expect(formats).to all(be_a described_class)
expect(formats.map(&:name)).to match_array(expected_formats)
end
end
context "for a 'Project' class" do
it_behaves_like "custom field formats",
"Project",
["bool", "date", "float", "int", "link", "list", "string", "text", "user", "version"]
end
context "for a 'WorkPackage' class" do
context "with a custom_field_hierarchies ee", with_ee: [:custom_field_hierarchies] do
it_behaves_like "custom field formats",
"WorkPackage",
["bool", "date", "float", "int", "link", "list", "string", "text", "user", "version", "hierarchy"]
end
context "without a custom_field_hierarchies ee" do
it_behaves_like "custom field formats",
"WorkPackage",
["bool", "date", "float", "int", "link", "list", "string", "text", "user", "version"]
end
end
context "for a 'Version' class" do
it_behaves_like "custom field formats",
"Version",
["bool", "date", "float", "int", "list", "string", "text", "user", "version"]
end
context "for a 'TimeEntry' class" do
it_behaves_like "custom field formats",
"TimeEntry",
["bool", "date", "float", "int", "list", "string", "text", "user", "version"]
end
context "for a 'User' class" do
it_behaves_like "custom field formats",
"User",
["bool", "date", "float", "int", "list", "string", "text"]
end
context "for a 'Group' class" do
it_behaves_like "custom field formats",
"Group",
["bool", "date", "float", "int", "list", "string", "text"]
end
end
describe ".available_formats" do
context "with a custom_field_hierarchies ee", with_ee: [:custom_field_hierarchies] do
it "returns all custom field formats including hierarchy" do
formats = described_class.available_formats
expect(formats)
.to contain_exactly("bool", "date", "float", "int", "link", "list", "string", "text", "user",
"version", "hierarchy", "empty")
end
end
context "without a custom_field_hierarchies ee" do
it "returns all custom field formats excluding hierarchy" do
formats = described_class.available_formats
expect(formats)
.to contain_exactly("bool", "date", "float", "int", "link", "list", "string", "text", "user",
"version", "empty")
end
end
end
end
@@ -398,12 +398,13 @@ RSpec.describe OpenProject::JournalFormatter::CustomField do
end
end
context "for hierarchy custom field" do
shared_let(:custom_field) { create(:hierarchy_wp_custom_field) }
shared_let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
shared_let(:root) { custom_field.hierarchy_root }
shared_let(:luke) { service.insert_item(parent: root, label: "luke", short: "LS").value! }
shared_let(:mara) { service.insert_item(parent: luke, label: "mara").value! }
context "for hierarchy custom field", with_ee: [:custom_field_hierarchies] do
let!(:custom_field) { build_stubbed(:hierarchy_wp_custom_field) }
let!(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
let!(:root) { custom_field.hierarchy_root }
let!(:luke) { service.insert_item(parent: root, label: "luke", short: "LS").value! }
let!(:mara) { service.insert_item(parent: luke, label: "mara").value! }
describe "first value being nil and second value a string" do
let(:values) { [nil, mara.id.to_s] }
@@ -31,7 +31,7 @@
require "spec_helper"
require Rails.root.join("db/migrate/20250102161733_adds_position_cache_to_hierarchy_items.rb")
RSpec.describe AddsPositionCacheToHierarchyItems, type: :model do
RSpec.describe AddsPositionCacheToHierarchyItems, type: :model, with_ee: [:custom_field_hierarchies] do
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe CustomField::Hierarchy::Item, :model do
RSpec.describe CustomField::Hierarchy::Item, :model, with_ee: [:custom_field_hierarchies] do
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
@@ -32,7 +32,7 @@ require "spec_helper"
RSpec.describe CustomField::OrderStatements do
# integration tests at spec/models/query/results_cf_sorting_integration_spec.rb
context "when hierarchy" do
context "when hierarchy", with_ee: [:custom_field_hierarchies] do
let(:service) { CustomFields::Hierarchy::HierarchicalItemService.new }
let(:item) { custom_field.hierarchy_root }
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe WorkPackageCustomField, :model do
RSpec.describe WorkPackageCustomField, :model, with_ee: [:custom_field_hierarchies] do
let(:feature) { create(:type_feature) }
let(:task) { create(:type_task) }
let(:project) { create(:project, types: [feature, task]) }
@@ -30,10 +30,10 @@
require "spec_helper"
RSpec.describe Queries::Operators::CustomFields::Hierarchies::EqualsWithDescendants do
RSpec.describe Queries::Operators::CustomFields::Hierarchies::EqualsWithDescendants, with_ee: [:custom_field_hierarchies] do
subject(:sql) { described_class.sql_for_field(values, db_table, db_field) }
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
let!(:root) { service.generate_root(custom_field).value! }
let!(:germany) { service.insert_item(parent: root, label: "Germany", short: "DE").value! }
let!(:berlin) { service.insert_item(parent: germany, label: "Berlin").value! }
@@ -182,7 +182,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
context "for list format" do
let(:possible_values) { %w[100 3 20] }
let(:id_by_value) { custom_field.possible_values.to_h { [_1.value, _1.id] } }
let(:id_by_value) { custom_field.possible_values.to_h { [it.value, it.id] } }
context "if not allowing multi select" do
include_examples "it sorts" do
@@ -194,7 +194,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
# sorting is done by position, and not by value
wp_with_cf_value(id_by_value.fetch("100")),
wp_with_cf_value(id_by_value.fetch("3")),
wp_with_cf_value(id_by_value.fetch("20")),
wp_with_cf_value(id_by_value.fetch("20"))
]
end
end
@@ -218,7 +218,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
wp_with_cf_value(id_by_value.fetch_values("20", "100")), # 100, 20
wp_with_cf_value(id_by_value.fetch_values("3")), # 3
wp_with_cf_value(id_by_value.fetch_values("3", "20")), # 3, 20
wp_with_cf_value(id_by_value.fetch_values("20")), # 20
wp_with_cf_value(id_by_value.fetch_values("20")) # 20
]
end
@@ -241,7 +241,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
create(:user, lastname: "A", firstname: "X", login: "ax", mail: "ax@o.p")
]
end
shared_let(:id_by_login) { users.to_h { [_1.login, _1.id] } }
shared_let(:id_by_login) { users.to_h { [it.login, it.id] } }
shared_let(:role) { create(:project_role) }
@@ -261,7 +261,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
wp_with_cf_value(id_by_login.fetch("ax")),
wp_with_cf_value(id_by_login.fetch("ba")),
wp_with_cf_value(id_by_login.fetch("bb1")),
wp_with_cf_value(id_by_login.fetch("bb2")),
wp_with_cf_value(id_by_login.fetch("bb2"))
]
end
end
@@ -279,7 +279,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
wp_with_cf_value(id_by_login.fetch_values("ax", "bb1")), # ax, bb1
wp_with_cf_value(id_by_login.fetch_values("ba")), # ba
wp_with_cf_value(id_by_login.fetch_values("bb1", "ba")), # ba, bb1
wp_with_cf_value(id_by_login.fetch_values("ba", "bb2")), # ba, bb2
wp_with_cf_value(id_by_login.fetch_values("ba", "bb2")) # ba, bb2
]
end
@@ -302,7 +302,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
create(:version, project:, sharing: "system", name: "9")
]
end
let(:id_by_name) { versions.to_h { [_1.name, _1.id] } }
let(:id_by_name) { versions.to_h { [it.name, it.id] } }
context "if not allowing multi select" do
include_examples "it sorts" do
@@ -332,7 +332,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
wp_with_cf_value(id_by_name.fetch_values("9", "10.10.10")), # 9, 10.10.10
wp_with_cf_value(id_by_name.fetch_values("10.2", "10.10.2")), # 10.2, 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.2")), # 10.10.2
wp_with_cf_value(id_by_name.fetch_values("10.10.10")), # 10.10.10
wp_with_cf_value(id_by_name.fetch_values("10.10.10")) # 10.10.10
]
end
@@ -346,7 +346,7 @@ RSpec.describe Query::Results, "Sorting by custom field" do
end
end
context "for hierarchy format" do
context "for hierarchy format", with_ee: [:custom_field_hierarchies] do
include_examples "it sorts" do
let(:custom_field) { create(:hierarchy_wp_custom_field, hierarchy_root: nil) }
let(:root) { service.generate_root(custom_field).value! }
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "API v3 custom field items", :webmock, content_type: :json do
RSpec.describe "API v3 custom field items", :webmock, content_type: :json, with_ee: [:custom_field_hierarchies] do
include API::V3::Utilities::PathHelper
let(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe "API v3 custom field hierarchy items", :webmock, content_type: :json do
RSpec.describe "API v3 custom field hierarchy items", :webmock, content_type: :json, with_ee: [:custom_field_hierarchies] do
include API::V3::Utilities::PathHelper
describe "GET /api/v3/custom_fields/:id/items" do
@@ -30,7 +30,7 @@
require "spec_helper"
RSpec.describe CustomFields::Hierarchy::HierarchicalItemService do
RSpec.describe CustomFields::Hierarchy::HierarchicalItemService, with_ee: [:custom_field_hierarchies] do
let!(:custom_field) { create(:custom_field, field_format: "hierarchy", hierarchy_root: nil) }
let!(:invalid_custom_field) { create(:custom_field, field_format: "text", hierarchy_root: nil) }
+1 -1
View File
@@ -31,7 +31,7 @@
module CustomFieldsHelpers
def factory_bot_custom_field_traits_for(class_name)
OpenProject::CustomFieldFormat
.all_for_class_name(class_name)
.available_for_class_name(class_name)
.flat_map do |format|
trait_name = trait_name(format.name)
[
@@ -68,6 +68,22 @@ module Pages
click_on type
end
def expect_having_create_item(type)
wait_for_network_idle
click_button "New custom field"
expect(page).to have_link(type)
end
def expect_not_having_create_item(type)
wait_for_network_idle
click_button "New custom field"
expect(page).to have_no_link(type)
end
def expect_none_listed
expect(page).to have_text("There are currently no custom fields.")
end