mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Restore the old customOptions table but within a separate "items" tab
This commit is contained in:
@@ -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) ||
|
||||
|
||||
@@ -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? &&
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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? %>
|
||||
|
||||
<div class="generic-table--container">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table" id="custom-options-table" data-controller="table-highlighting">
|
||||
<colgroup>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col data-highlight="false">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t("activerecord.attributes.custom_value.value") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t(:label_default) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= t(:button_sort) %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th style="max-width:200px;">
|
||||
<div class="generic-table--empty-header">
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
data-test-selector="dragula-container"
|
||||
data-admin--custom-fields-target="dragContainer">
|
||||
<% custom_field.custom_options.each_with_index do |custom_option, i| %>
|
||||
<%= f.fields_for :custom_options, custom_option do |co_f| %>
|
||||
<tr
|
||||
class="dragula-element custom-option-row"
|
||||
data-admin--custom-fields-target="customOptionRow">
|
||||
<td>
|
||||
<span class="dragula-handle icon icon-table icon-drag-handle"></span>
|
||||
<%= co_f.hidden_field :id,
|
||||
class: "custom-option-id" %>
|
||||
<span class="custom-option-value">
|
||||
<%= co_f.text_field :value,
|
||||
no_label: true %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<%= 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 %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="reorder-icons">
|
||||
<a
|
||||
title="<%= t(:label_sort_highest) %>"
|
||||
rel="nofollow"
|
||||
href="#"
|
||||
class="sort-up-custom-option"
|
||||
data-action="admin--custom-fields#moveRowToTheTop">
|
||||
<%= op_icon("icon-context icon-sort-up icon-small") %>
|
||||
</a>
|
||||
<a
|
||||
title="<%= t(:label_sort_higher) %>"
|
||||
rel="nofollow"
|
||||
href="#"
|
||||
class="move-up-custom-option"
|
||||
data-action="admin--custom-fields#moveRowUp">
|
||||
<%= op_icon("icon-context icon-arrow-up2 icon-small") %>
|
||||
</a>
|
||||
<a
|
||||
title="<%= t(:label_sort_lower) %>"
|
||||
rel="nofollow" href="#"
|
||||
class="move-down-custom-option"
|
||||
data-action="admin--custom-fields#moveRowDown">
|
||||
<%= op_icon("icon-context icon-arrow-down2 icon-small") %>
|
||||
</a>
|
||||
<a
|
||||
title="<%= t(:label_sort_lowest) %>"
|
||||
rel="nofollow"
|
||||
href="#"
|
||||
class="sort-down-custom-option"
|
||||
data-action="admin--custom-fields#moveRowToTheBottom">
|
||||
<%= op_icon("icon-context icon-sort-down icon-small") %>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<%= 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) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%=
|
||||
render Primer::Beta::Button.new(
|
||||
scheme: :link,
|
||||
test_selector: "add-custom-option",
|
||||
data: { action: "admin--custom-fields#addOption" },
|
||||
mt: 2
|
||||
) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t(:button_add)
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,69 @@
|
||||
<%#-- 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), "#{CustomField.model_name.human} #{h @custom_field.name}", t(:label_item_plural) %>
|
||||
|
||||
<%= render(Admin::CustomFields::EditFormHeaderComponent.new(custom_field: @custom_field, selected: :items)) %>
|
||||
|
||||
<%= settings_primer_form_with(
|
||||
model: @custom_field,
|
||||
scope: :custom_field,
|
||||
url: custom_field_path(@custom_field),
|
||||
html: { 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_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 %>
|
||||
@@ -389,7 +389,7 @@ en:
|
||||
contained_in_type: "Contained in type"
|
||||
confirm_destroy_option: "Deleting an option will delete all of its occurrences (e.g. in work packages). Are you sure you want to delete it?"
|
||||
reorder_alphabetical: "Reorder values alphabetically"
|
||||
reorder_confirmation: "Warning: The current order of available values will be lost. Continue?"
|
||||
reorder_confirmation: "Warning: The current order of available values as well as all unsaved values will be lost. Are you sure you want to continue?"
|
||||
placeholder_version_select: "Work package or project selection is required first"
|
||||
calculated_field_not_editable: "Non-editable attribute. This value is calculated automatically."
|
||||
no_role_assigment: "No role assignment"
|
||||
|
||||
@@ -218,6 +218,8 @@ Rails.application.routes.draw do
|
||||
|
||||
get :attribute_help_text
|
||||
put :update_attribute_help_text
|
||||
|
||||
get :list_items
|
||||
end
|
||||
|
||||
scope module: :admin do
|
||||
@@ -694,6 +696,8 @@ Rails.application.routes.draw do
|
||||
|
||||
get :attribute_help_text
|
||||
put :update_attribute_help_text
|
||||
|
||||
get :list_items
|
||||
end
|
||||
|
||||
resources :items, controller: "/admin/settings/project_custom_fields/hierarchy/items" do
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* -- 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.
|
||||
* ++
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import dragula from 'dragula';
|
||||
|
||||
export default class CustomFieldsController extends Controller {
|
||||
static targets = [
|
||||
'dragContainer',
|
||||
|
||||
'customOptionDefaults',
|
||||
'customOptionRow',
|
||||
];
|
||||
|
||||
static values = {
|
||||
multiSelect: Boolean,
|
||||
};
|
||||
|
||||
declare readonly multiSelectValue:boolean;
|
||||
|
||||
declare readonly dragContainerTarget:HTMLElement;
|
||||
declare readonly hasDragContainerTarget:boolean;
|
||||
|
||||
declare readonly customOptionDefaultsTargets:HTMLInputElement[];
|
||||
declare readonly customOptionRowTargets:HTMLTableRowElement[];
|
||||
|
||||
connect() {
|
||||
if (this.hasDragContainerTarget) {
|
||||
this.setupDragAndDrop();
|
||||
}
|
||||
}
|
||||
|
||||
moveRowUp(event:{ target:HTMLElement }) {
|
||||
const row = event.target.closest('tr')!;
|
||||
const idx = this.customOptionRowTargets.indexOf(row);
|
||||
if (idx > 0) {
|
||||
this.customOptionRowTargets[idx - 1].before(row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
moveRowDown(event:{ target:HTMLElement }) {
|
||||
const row = event.target.closest('tr')!;
|
||||
const idx = this.customOptionRowTargets.indexOf(row);
|
||||
if (idx < this.customOptionRowTargets.length - 1) {
|
||||
this.customOptionRowTargets[idx + 1].after(row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
moveRowToTheTop(event:{ target:HTMLElement }) {
|
||||
const row = event.target.closest('tr')!;
|
||||
const first = this.customOptionRowTargets[0];
|
||||
|
||||
if (first && first !== row) {
|
||||
first.before(row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
moveRowToTheBottom(event:{ target:HTMLElement }) {
|
||||
const row = event.target.closest('tr')!;
|
||||
const last = this.customOptionRowTargets[this.customOptionRowTargets.length - 1];
|
||||
|
||||
if (last && last !== row) {
|
||||
last.after(row);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
removeOption(event:MouseEvent) {
|
||||
const self = event.target as HTMLAnchorElement;
|
||||
if (self.href === '#' || self.href.endsWith('/0')) {
|
||||
const row = self.closest('tr');
|
||||
|
||||
if (row && this.customOptionRowTargets.length > 1) {
|
||||
row.remove();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
return true; // send off deletion
|
||||
}
|
||||
|
||||
addOption() {
|
||||
const count = this.customOptionRowTargets.length;
|
||||
const last = this.customOptionRowTargets[count - 1];
|
||||
const dup = last.cloneNode(true) as HTMLElement;
|
||||
|
||||
const input = dup.querySelector('.custom-option-value input') as HTMLInputElement;
|
||||
|
||||
input.setAttribute('name', `custom_field[custom_options_attributes][${count}][value]`);
|
||||
input.setAttribute('id', `custom_field_custom_options_attributes_${count}_value`);
|
||||
input.value = '';
|
||||
|
||||
dup
|
||||
.querySelector('.custom-option-id')
|
||||
?.remove();
|
||||
|
||||
const defaultValueCheckbox = dup.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
const defaultValueHidden = dup.querySelector('input[type="hidden"]') as HTMLInputElement;
|
||||
|
||||
defaultValueHidden.setAttribute('name', `custom_field[custom_options_attributes][${count}][default_value]`);
|
||||
defaultValueHidden.removeAttribute('id');
|
||||
defaultValueCheckbox.setAttribute('name', `custom_field[custom_options_attributes][${count}][default_value]`);
|
||||
defaultValueCheckbox.setAttribute('id', `custom_field_custom_options_attributes_${count}_default_value`);
|
||||
defaultValueCheckbox.checked = false;
|
||||
|
||||
last.insertAdjacentElement('afterend', dup);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
uncheckOtherDefaults(event:{ target:HTMLElement }) {
|
||||
const cb = event.target as HTMLInputElement;
|
||||
|
||||
if (cb.checked && !this.multiSelectValue) {
|
||||
this.customOptionDefaultsTargets.forEach((el) => (el.checked = false));
|
||||
cb.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
private setupDragAndDrop() {
|
||||
// Make custom fields draggable
|
||||
const drake = dragula([this.dragContainerTarget], {
|
||||
isContainer: () => false,
|
||||
moves: (el, source, handle:HTMLElement) => handle.classList.contains('dragula-handle'),
|
||||
accepts: () => true,
|
||||
invalid: () => false,
|
||||
direction: 'vertical',
|
||||
copy: false,
|
||||
copySortSource: false,
|
||||
revertOnSpill: true,
|
||||
removeOnSpill: false,
|
||||
mirrorContainer: this.dragContainerTarget,
|
||||
ignoreInputTextSelection: true,
|
||||
});
|
||||
|
||||
// Setup autoscroll
|
||||
void window.OpenProject.getPluginContext().then((pluginContext) => {
|
||||
new pluginContext.classes.DomAutoscrollService(
|
||||
[
|
||||
document.getElementById('content-body')!,
|
||||
],
|
||||
{
|
||||
margin: 25,
|
||||
maxSpeed: 10,
|
||||
scrollWhenOutside: true,
|
||||
autoScroll: () => drake.dragging,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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 CustomFields::DetailsComponent, type: :component do
|
||||
describe ".supported?" do
|
||||
context "with a bool cf" do
|
||||
let(:custom_field) { build_stubbed(:boolean_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a string cf" do
|
||||
let(:custom_field) { build_stubbed(:string_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a text cf" do
|
||||
let(:custom_field) { build_stubbed(:text_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a link cf" do
|
||||
let(:custom_field) { build_stubbed(:link_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with an int cf" do
|
||||
let(:custom_field) { build_stubbed(:integer_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a version cf" do
|
||||
let(:custom_field) { build_stubbed(:version_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a user cf" do
|
||||
let(:custom_field) { build_stubbed(:user_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a date cf" do
|
||||
let(:custom_field) { build_stubbed(:date_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a list cf" do
|
||||
let(:custom_field) { build_stubbed(:list_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a float cf" do
|
||||
let(:custom_field) { build_stubbed(:float_wp_custom_field) }
|
||||
|
||||
it "is not supported" do
|
||||
expect(described_class).not_to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a calculated_value cf" do
|
||||
let(:custom_field) { build_stubbed(:calculated_value_project_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a hierarchy cf" do
|
||||
let(:custom_field) { build_stubbed(:hierarchy_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a weighted_item_list cf" do
|
||||
let(:custom_field) { build_stubbed(:weighted_item_list_wp_custom_field) }
|
||||
|
||||
it "is supported" do
|
||||
expect(described_class).to be_supported(custom_field)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user