diff --git a/app/components/admin/custom_fields/edit_form_header_component.rb b/app/components/admin/custom_fields/edit_form_header_component.rb index 85a0319df35..46c9ac4fd79 100644 --- a/app/components/admin/custom_fields/edit_form_header_component.rb +++ b/app/components/admin/custom_fields/edit_form_header_component.rb @@ -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) || diff --git a/app/components/custom_fields/details_component.rb b/app/components/custom_fields/details_component.rb index 6d3e345a815..31b51f12b82 100644 --- a/app/components/custom_fields/details_component.rb +++ b/app/components/custom_fields/details_component.rb @@ -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? && diff --git a/app/components/settings/project_custom_fields/edit_form_header_component.rb b/app/components/settings/project_custom_fields/edit_form_header_component.rb index 4f52c6e58d1..3a2bda6d415 100644 --- a/app/components/settings/project_custom_fields/edit_form_header_component.rb +++ b/app/components/settings/project_custom_fields/edit_form_header_component.rb @@ -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? diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index 4e5c6ac031d..75c3460e77e 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -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 diff --git a/app/controllers/concerns/custom_fields/shared_actions.rb b/app/controllers/concerns/custom_fields/shared_actions.rb index 625a1e7e369..8cd591d82a4 100644 --- a/app/controllers/concerns/custom_fields/shared_actions.rb +++ b/app/controllers/concerns/custom_fields/shared_actions.rb @@ -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 diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 37180bfc507..70bc7741c7d 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -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 diff --git a/app/forms/custom_fields/details_form.rb b/app/forms/custom_fields/details_form.rb index 4eaf8771b8b..57d4f747f26 100644 --- a/app/forms/custom_fields/details_form.rb +++ b/app/forms/custom_fields/details_form.rb @@ -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? diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index 55b4d5b62e7..9ed29fbb74c 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -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 }, diff --git a/app/views/admin/settings/project_custom_fields/list_items.html.erb b/app/views/admin/settings/project_custom_fields/list_items.html.erb new file mode 100644 index 00000000000..f0adfc60203 --- /dev/null +++ b/app/views/admin/settings/project_custom_fields/list_items.html.erb @@ -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 %> diff --git a/app/views/custom_fields/_custom_options.html.erb b/app/views/custom_fields/_custom_options.html.erb new file mode 100644 index 00000000000..b122d871e5a --- /dev/null +++ b/app/views/custom_fields/_custom_options.html.erb @@ -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? %> + +
+
+ + + + + + + + + + + + + + + + + + <% custom_field.custom_options.each_with_index do |custom_option, i| %> + <%= f.fields_for :custom_options, custom_option do |co_f| %> + + + + + + + <% end %> + <% end %> + +
+
+
+ + <%= t("activerecord.attributes.custom_value.value") %> + +
+
+
+
+
+ + <%= t(:label_default) %> + +
+
+
+
+
+ + <%= t(:button_sort) %> + +
+
+
+
+
+
+ + <%= co_f.hidden_field :id, + class: "custom-option-id" %> + + <%= co_f.text_field :value, + no_label: true %> + + + <%= 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 %> + + + + <%= op_icon("icon-context icon-sort-up icon-small") %> + + + <%= op_icon("icon-context icon-arrow-up2 icon-small") %> + + + <%= op_icon("icon-context icon-arrow-down2 icon-small") %> + + + <%= op_icon("icon-context icon-sort-down icon-small") %> + + + + <%= 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) %> +
+ +
+
+ +<%= + 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 +%> diff --git a/app/views/custom_fields/list_items.html.erb b/app/views/custom_fields/list_items.html.erb new file mode 100644 index 00000000000..b284ab04ed0 --- /dev/null +++ b/app/views/custom_fields/list_items.html.erb @@ -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 %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a00652d50f1..bcaa3256caa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 5373a93f1f3..ef6e89695a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts b/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts new file mode 100644 index 00000000000..e9039f7336f --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/admin/custom-fields.controller.ts @@ -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, + }, + ); + }); + } +} diff --git a/spec/components/custom_fields/details_component_spec.rb b/spec/components/custom_fields/details_component_spec.rb deleted file mode 100644 index 021cc262dfe..00000000000 --- a/spec/components/custom_fields/details_component_spec.rb +++ /dev/null @@ -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