mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: [] }
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user