Feature 50526, support for non-open versions in version custom fields

- adds `allow_non_open_versions` boolean flag/column for CFs
- this option is available for CFs of type Version
- when enabled, edit offers also locked and closed versions as values
This commit is contained in:
Richard "Virgo" Richter
2023-10-19 12:37:24 +00:00
parent 2422f45d09
commit d646464fcf
16 changed files with 320 additions and 6 deletions
@@ -35,7 +35,11 @@ module AssignableCustomFieldValues
when 'list'
custom_field.possible_values
when 'version'
assignable_versions
if custom_field.allow_non_open_versions?
all_versions
else
assignable_versions
end
end
end
end
@@ -47,5 +47,6 @@ module CustomFields
attribute :possible_values
attribute :multi_value
attribute :content_right_to_left
attribute :allow_non_open_versions
end
end
+1
View File
@@ -67,6 +67,7 @@ module Projects
end
delegate :assignable_versions, to: :model
delegate :all_versions, to: :model
def assignable_status_codes
Project.status_codes.keys
@@ -189,6 +189,10 @@ module WorkPackages
model.try(:assignable_versions) if model.project
end
def all_versions
model.try(:all_versions) if model.project
end
def assignable_budgets
model.project&.budgets
end
+12
View File
@@ -264,6 +264,10 @@ class CustomField < ApplicationRecord
field_format == "list"
end
def version?
field_format == "version"
end
def formattable?
field_format == "text"
end
@@ -281,6 +285,14 @@ class CustomField < ApplicationRecord
[ProjectCustomField, WorkPackageCustomField].include?(self.class)
end
def allow_non_open_versions?
allow_non_open_versions
end
def allow_non_open_versions_possible?
version? && [ProjectCustomField, WorkPackageCustomField].include?(self.class)
end
##
# Overrides cache key so that a custom field's representation
# is updated correctly when it's mutli_value attribute changes.
+1
View File
@@ -466,6 +466,7 @@ class PermittedParams
:possible_values,
:multi_value,
:content_right_to_left,
:allow_non_open_versions,
{ custom_options_attributes: %i(id value default_value position) },
{ type_ids: [] }
],
+6
View File
@@ -274,6 +274,12 @@ class Project < ApplicationRecord
@assignable_versions ||= shared_versions.references(:project).with_status_open.order_by_semver_name.to_a
end
# Returns all versions, including closed and locked, as an array.
# This is useful for custom fields allowing non-open versions as input values.
def all_versions
@all_versions ||= shared_versions.references(:project).order_by_semver_name.to_a
end
# Returns an AR scope of all custom fields enabled for project's work packages
# (explicitly associated custom fields and custom fields enabled for all projects)
def all_work_package_custom_fields
+4
View File
@@ -262,6 +262,10 @@ class WorkPackage < ApplicationRecord
end
end
def all_versions
@all_versions ||= project&.all_versions
end
def to_s
"#{type.is_standard ? '' : type.name} ##{id}: #{subject}"
end
+10
View File
@@ -105,6 +105,16 @@ See COPYRIGHT and LICENSE files for more details.
</div>
</fieldset>
<% end %>
<% if @custom_field.new_record? || @custom_field.version? || @custom_field.allow_non_open_versions_possible? %>
<div class="form--field" hidden>
<%= f.check_box :allow_non_open_versions,
data: {
'admin--custom-fields-target': 'allowNonOpenVersions'
} %>
</div>
<% end %>
<div data-admin--custom-fields-target="defaultValue">
<div class="form--field" id="default_value_text">
<% if @custom_field.new_record? || !%w[text bool].include?(@custom_field.field_format) %>
+1
View File
@@ -538,6 +538,7 @@ en:
custom_action:
actions: "Actions"
custom_field:
allow_non_open_versions: "Allow non-open versions"
default_value: "Default value"
editable: "Editable"
field_format: "Format"
@@ -0,0 +1,5 @@
class AddAllowNonOpenVersionsToCustomFields < ActiveRecord::Migration[7.0]
def change
add_column :custom_fields, :allow_non_open_versions, :boolean, default: false
end
end
@@ -37,6 +37,7 @@ export default class CustomFieldsController extends Controller {
'length',
'regexp',
'multiSelect',
'allowNonOpenVersions',
'possibleValues',
'defaultValue',
'defaultText',
@@ -61,6 +62,7 @@ export default class CustomFieldsController extends Controller {
declare readonly lengthTargets:HTMLElement[];
declare readonly regexpTargets:HTMLElement[];
declare readonly multiSelectTargets:HTMLElement[];
declare readonly allowNonOpenVersionsTargets:HTMLElement[];
declare readonly possibleValuesTargets:HTMLElement[];
declare readonly defaultValueTargets:HTMLElement[];
declare readonly defaultTextTargets:HTMLElement[];
@@ -272,6 +274,7 @@ export default class CustomFieldsController extends Controller {
this.activate(this.defaultBoolTargets, false);
this.activate(this.defaultLongTextTargets, false);
this.activate(this.multiSelectTargets, false);
this.activate(this.allowNonOpenVersionsTargets, false);
this.activate(this.textOrientationTargets, false);
this.activate(this.defaultValueTargets);
this.activate(this.defaultTextTargets);
@@ -311,7 +314,7 @@ export default class CustomFieldsController extends Controller {
this.unsearchable();
break;
case 'version':
this.show(...this.multiSelectTargets);
this.show(...this.multiSelectTargets, ...this.allowNonOpenVersionsTargets);
this.activate(this.defaultValueTargets, false);
this.activate(this.possibleValuesTargets, false);
this.hide(...this.lengthTargets, ...this.regexpTargets, ...this.defaultValueTargets);
@@ -53,6 +53,7 @@ module API
:assignable_categories,
:assignable_priorities,
:assignable_versions,
:all_versions,
:assignable_budgets,
to: :contract
@@ -83,6 +83,11 @@ module TimeEntries
work_package.try(:assignable_versions) || project.try(:assignable_versions) || []
end
# Necessary for custom fields of type version with allow non-open enabled.
def all_versions
work_package.try(:all_versions) || project.try(:all_versions) || []
end
private
def validate_work_package
@@ -9,8 +9,8 @@ RSpec.describe 'custom fields', js: true, with_cuprite: true do
login_as user
end
shared_examples "creating a new list custom field" do |type|
it "creates a new list custom field with its options in the right order" do
shared_examples "creating a new custom field" do |type|
it "has the options in the right order for a list custom field" do
cf_page.visit_tab type
click_on "Create a new custom field"
@@ -61,14 +61,105 @@ RSpec.describe 'custom fields', js: true, with_cuprite: true do
expect(page).to have_field("custom_field_custom_options_attributes_1_default_value", checked: true)
expect(page).to have_field("custom_field_custom_options_attributes_2_default_value", checked: false)
end
it "shows the right options for each custom field type" do
cf_page.visit_tab type
click_on "Create a new custom field"
wait_for_reload
cf_page.set_name "Ignored"
# Form element labels, default English translation in the trailing comment:
label_min_length = I18n.t('activerecord.attributes.custom_field.min_length') # Minimum length
label_max_length = I18n.t('activerecord.attributes.custom_field.max_length') # Maximum length
label_regexp = I18n.t('activerecord.attributes.custom_field.regexp') # Regular expression
label_multi_value = I18n.t('activerecord.attributes.custom_field.multi_value') # Allow multi-select
label_allow_non_open_versions = I18n.t('activerecord.attributes.custom_field.allow_non_open_versions') # Allow non-open versions
label_possible_values = I18n.t('activerecord.attributes.custom_field.possible_values').upcase # Possible values, capitalized on UI
label_default_value = I18n.t('activerecord.attributes.custom_field.default_value') # Default value
label_is_required = I18n.t('activerecord.attributes.custom_field.is_required') # Required
label_searchable = I18n.t('activerecord.attributes.custom_field.searchable') # Searchable
# Project CFs don't show "For all projects" and "Used as a filter". Not tested here.
# Content right to left is not shown for Project CFs Long text. Strange. Not tested.
def expect_page_to_have_texts(*text)
text.each do |t|
expect(page).to have_text(t)
end
end
def expect_page_not_to_have_texts(*text)
text.each do |t|
expect(page).not_to have_text(t)
end
end
select "Text", from: "custom_field_field_format"
expect_page_to_have_texts(
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required, label_searchable)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values)
select "Long text", from: "custom_field_field_format"
expect_page_to_have_texts(
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required, label_searchable)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values)
# Both Integer and Float have min/max_len and regex as well which seems strange.
select "Integer", from: "custom_field_field_format"
expect_page_to_have_texts(
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values, label_searchable)
select "Float", from: "custom_field_field_format"
expect_page_to_have_texts(
label_min_length, label_max_length, label_regexp, label_default_value, label_is_required)
expect_page_not_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_possible_values, label_searchable)
select "List", from: "custom_field_field_format"
expect_page_to_have_texts(
label_multi_value, label_possible_values, label_is_required, label_searchable)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions, label_default_value)
select "Date", from: "custom_field_field_format"
expect_page_to_have_texts(label_is_required)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_multi_value,
label_allow_non_open_versions, label_possible_values, label_default_value, label_searchable)
select "Boolean", from: "custom_field_field_format"
expect_page_to_have_texts(
label_default_value, label_is_required)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_multi_value,
label_allow_non_open_versions, label_possible_values, label_searchable)
select "User", from: "custom_field_field_format"
expect_page_to_have_texts(
label_multi_value, label_is_required)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp, label_allow_non_open_versions,
label_possible_values, label_default_value, label_searchable)
select "Version", from: "custom_field_field_format"
expect_page_to_have_texts(
label_multi_value, label_allow_non_open_versions, label_is_required)
expect_page_not_to_have_texts(
label_min_length, label_max_length, label_regexp,
label_possible_values, label_default_value, label_searchable)
end
end
describe 'projects' do
it_behaves_like "creating a new list custom field", 'Projects'
it_behaves_like "creating a new custom field", 'Projects'
end
describe 'work packages' do
it_behaves_like "creating a new list custom field", 'Work packages'
it_behaves_like "creating a new custom field", 'Work packages'
end
context "with an existing list custom field" do
@@ -0,0 +1,165 @@
require "spec_helper"
require "support/pages/work_packages/abstract_work_package"
RSpec.describe "support for non-open version values in version custom field", :js, :with_cuprite do
shared_let(:admin) { create(:admin) }
let(:current_user) { admin }
let(:wp_page) { Pages::FullWorkPackage.new work_package }
let(:cf_edit_field) do
field = wp_page.edit_field custom_field.attribute_name(:camel_case)
field.field_type = 'create-autocompleter'
field
end
let(:work_package) { create(:work_package, project:, type:) }
let!(:version_closed) { create(:version, project:, name: 'Version Closed', status: 'closed') }
let!(:version_locked) { create(:version, project:, name: 'Version Locked', status: 'locked') }
let!(:version_open) { create(:version, project:, name: 'Version Open', status: 'open') }
shared_let(:type) { create(:type) }
shared_let(:project) { create(:project, types: [type]) }
shared_let(:role) { create(:project_role) }
shared_let(:custom_field) do
create(
:version_wp_custom_field,
name: "Affected version",
multi_value: false,
allow_non_open_versions: true,
types: [type],
projects: [project]
)
end
before do
login_as current_user
wp_page.visit!
wp_page.ensure_page_loaded
end
it "is shown and allowed to be updated with open or non-open version" do
expect(page).to have_text custom_field.name
cf_edit_field.activate!
expect(page).to have_text "Version Open"
expect(page).to have_text "Version Locked"
expect(page).to have_text "Version Closed"
cf_edit_field.set_value "Version Locked"
wp_page.expect_and_dismiss_toaster(message: "Successful update.")
expect(page).to have_text custom_field.name
expect(page).not_to have_text "Version Open"
expect(page).to have_text "Version Locked"
expect(page).not_to have_text "Version Closed"
work_package.reload
# only one value, so no array
cvs = work_package
.custom_value_for(custom_field)
.typed_value
expect(cvs).to eq version_locked
# Let's check edit and both closed and open versions as well:
cf_edit_field.activate!
cf_edit_field.set_value "Version Closed"
wp_page.expect_and_dismiss_toaster(message: "Successful update.")
expect(page).to have_text "Version Closed"
cf_edit_field.activate!
cf_edit_field.set_value "Version Open"
wp_page.expect_and_dismiss_toaster(message: "Successful update.")
expect(page).to have_text "Version Open"
work_package.reload
cvs = work_package
.custom_value_for(custom_field)
.typed_value
expect(cvs).to eq version_open
end
context "with multi-value version field" do
shared_let(:custom_field) do
create(
:version_wp_custom_field,
name: "Affected versions",
multi_value: true,
allow_non_open_versions: true,
types: [type],
projects: [project]
)
end
it "is shown and allowed to be updated with open or non-open version" do
expect(page).to have_text custom_field.name
# First we set mix of open and non-open values
cf_edit_field.activate!
expect(page).to have_text "Version Open"
expect(page).to have_text "Version Locked"
expect(page).to have_text "Version Closed"
cf_edit_field.set_value "Version Locked"
cf_edit_field.set_value "Version Open"
cf_edit_field.submit_by_dashboard
wp_page.expect_and_dismiss_toaster(message: "Successful update.")
expect(page).to have_text custom_field.name
expect(page).to have_text "Version Open"
expect(page).to have_text "Version Locked"
expect(page).not_to have_text "Version Closed"
work_package.reload
cvs = work_package
.custom_value_for(custom_field)
.map(&:typed_value)
expect(cvs).to contain_exactly(version_open, version_locked)
# Update with a single non-open value
cf_edit_field.activate!
cf_edit_field.unset_value "Version Open", multi: true
cf_edit_field.unset_value "Version Locked", multi: true
cf_edit_field.set_value "Version Closed"
cf_edit_field.submit_by_dashboard
wp_page.expect_and_dismiss_toaster(message: "Successful update.")
expect(page).to have_text "Version Closed"
work_package.reload
# only one value, so no array
cvs = work_package
.custom_value_for(custom_field)
.typed_value
expect(cvs).to eq version_closed
end
end
context "with non-open values disabled" do
shared_let(:custom_field) do
create(
:version_wp_custom_field,
name: "Affected versions",
multi_value: false, # this doesn't matter that much, it's the same for single and multi-values
allow_non_open_versions: false,
types: [type],
projects: [project]
)
end
it "is shown but non-open version are not shown as options" do
expect(page).to have_text custom_field.name
# We'll just check the options and nothing more, the rest is checked elsewhere
cf_edit_field.activate!
expect(page).to have_text "Version Open"
expect(page).not_to have_text "Version Locked"
expect(page).not_to have_text "Version Closed"
end
end
end