├── .gitignore ├── assets ├── javascripts │ └── custom_fields_groups.js ├── images │ ├── textfields_group.png │ └── textfields_group.xcf └── stylesheets │ └── custom_fields_groups.css ├── Gemfile ├── config ├── routes.rb └── locales │ ├── ja.yml │ ├── en.yml │ └── de.yml ├── .editorconfig ├── test ├── test_helper.rb ├── unit │ ├── custom_fields_group_field_test.rb │ └── custom_fields_group_test.rb ├── fixtures │ ├── custom_fields_groups.yml │ └── custom_fields_group_fields.yml ├── system │ └── fieldset_test.rb ├── functional │ └── custom_fields_groups_controller_test.rb └── integration │ └── layout_test.rb ├── app ├── views │ ├── custom_fields_groups │ │ ├── _form.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _custom_fields_group.html.erb │ │ └── index.html.erb │ ├── settings │ │ └── _redmine_custom_fields_groups.html.erb │ └── issues │ │ └── _form_custom_fields.html.erb ├── models │ ├── custom_fields_group_field.rb │ └── custom_fields_group.rb ├── overrides │ └── issues.rb ├── helpers │ └── custom_fields_groups_helper.rb └── controllers │ └── custom_fields_groups_controller.rb ├── lib ├── redmine_custom_fields_groups.rb └── redmine_custom_fields_groups │ ├── options.rb │ ├── hooks │ ├── view_layouts_base_html_head_hook.rb │ └── view_user_preferences_hook.rb │ └── patches │ ├── user_preference_patch.rb │ └── issues_helper_patch.rb ├── db └── migrate │ ├── 20240409124454_add_on_delete_cascade_to_foreign_key_custom_field.rb │ ├── 20211011081234_create_custom_fields_groups.rb │ └── 20211011081244_create_custom_fields_group_fields.rb ├── README.md ├── init.rb ├── .github └── workflows │ └── test.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /assets/javascripts/custom_fields_groups.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'deface' 4 | -------------------------------------------------------------------------------- /assets/images/textfields_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtt-project/redmine_custom_fields_groups/HEAD/assets/images/textfields_group.png -------------------------------------------------------------------------------- /assets/images/textfields_group.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtt-project/redmine_custom_fields_groups/HEAD/assets/images/textfields_group.xcf -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Plugin's routes 2 | # See: http://guides.rubyonrails.org/routing.html 3 | 4 | resources :custom_fields_groups, except: %i[show] 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require_relative '../../../test/test_helper' 3 | 4 | ActiveRecord::FixtureSet.create_fixtures( 5 | File.dirname(__FILE__) + '/fixtures', 6 | ['custom_fields_groups', 'custom_fields_group_fields'] 7 | ) 8 | -------------------------------------------------------------------------------- /app/views/custom_fields_groups/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= error_messages_for 'custom_fields_group' %> 2 | 3 |
<%= f.text_field :name, required: true, size: 25 %>
5 |<%= group_fields_edit_tag @custom_fields_group %> 6 |
6 | <%= submit_tag l :button_save %> 7 |
8 | <% end %> 9 | -------------------------------------------------------------------------------- /db/migrate/20211011081244_create_custom_fields_group_fields.rb: -------------------------------------------------------------------------------- 1 | class CreateCustomFieldsGroupFields < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :custom_fields_group_fields, id: false do |t| 4 | t.references :custom_fields_group, index: true, foreign_key: true 5 | t.references :custom_field, index: { unique: true }, foreign_key: true, type: :integer 6 | 7 | # t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/custom_fields_groups/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= title [l(:label_custom_fields_group_plural), custom_fields_groups_path], l(:label_custom_fields_group_new) %> 2 | 3 | <%= labelled_form_for :custom_fields_group, @custom_fields_group, url: custom_fields_groups_path do |f| %> 4 | <%= render partial: 'form', locals: { f: f } %> 5 |6 | <%= submit_tag l :button_create %> 7 | <%= submit_tag l(:button_create_and_continue), name: 'continue' %> 8 |
9 | <% end %> 10 | -------------------------------------------------------------------------------- /lib/redmine_custom_fields_groups/options.rb: -------------------------------------------------------------------------------- 1 | module RedmineCustomFieldsGroups 2 | class Options 3 | include Redmine::I18n 4 | def self.group_tags 5 | [ 6 | [l(:label_group_tag_h3), "h3"], 7 | [l(:label_group_tag_h4), "h4"], 8 | [l(:label_group_tag_fieldset), "fieldset"] 9 | ] 10 | end 11 | def self.fieldset_states 12 | [ 13 | [l(:label_fieldset_state_all_expended), "all_expended"], 14 | [l(:label_fieldset_state_all_collapsed), "all_collapsed"], 15 | ] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/custom_fields_group.rb: -------------------------------------------------------------------------------- 1 | class CustomFieldsGroup < (defined?(ApplicationRecord) == 'constant' ? ApplicationRecord : ActiveRecord::Base) 2 | include Redmine::SafeAttributes 3 | 4 | validates :name, presence: true, uniqueness: true 5 | 6 | has_many :custom_fields_group_fields, :dependent => :delete_all 7 | has_many :custom_fields, :through => :custom_fields_group_fields 8 | 9 | acts_as_positioned 10 | scope :sorted, ->{ order :position } 11 | 12 | safe_attributes( 13 | 'name', 14 | 'position', 15 | 'custom_field_ids' 16 | ) 17 | end 18 | -------------------------------------------------------------------------------- /assets/stylesheets/custom_fields_groups.css: -------------------------------------------------------------------------------- 1 | #admin-menu a.custom-fields-groups { background-image: url(../images/textfields_group.png);} 2 | 3 | h3.custom-fields-groups { 4 | background: #0001; 5 | border-bottom: 3px solid black; 6 | padding: 0.4em; 7 | } 8 | 9 | h4.custom-fields-groups { 10 | background: #0001; 11 | padding: 0.3em; 12 | } 13 | 14 | fieldset.collapsible.custom-fields-groups { 15 | border-width: 1px; 16 | margin-top: 5px; 17 | } 18 | 19 | fieldset.collapsible.custom-fields-groups>legend { 20 | font-size: 15px; 21 | font-weight: bold; 22 | } 23 | -------------------------------------------------------------------------------- /app/views/custom_fields_groups/_custom_fields_group.html.erb: -------------------------------------------------------------------------------- 1 |4 | <%= content_tag(:label, l(:label_custom_fields_group_tag)) %> 5 | <%= select_tag('settings[custom_fields_group_tag]', 6 | options_for_select( 7 | RedmineCustomFieldsGroups::Options::group_tags, 8 | @settings['custom_fields_group_tag'].to_s)) %> 9 |
10 |11 | <%= content_tag(:label, l(:label_fieldset_default_state)) %> 12 | <%= select_tag('settings[fieldset_default_state]', 13 | options_for_select( 14 | RedmineCustomFieldsGroups::Options::fieldset_states, 15 | @settings['fieldset_default_state'].to_s)) %> 16 |
17 || <%= l(:field_name) %> | 12 |<%= l(:label_custom_field_plural) %> | 13 |14 | |
|---|
<%= l :label_no_data %>
23 | <% end %> 24 | 25 | <%= javascript_tag do %> 26 | $(function() { $("table.custom_fields_groups tbody").positionedItems(); }); 27 | <% end %> 28 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | # German strings go here for Rails i18n 2 | de: 3 | error_can_not_delete_custom_fields_group: Die Gruppe der benutzerdefinierten Felder 4 | kann nicht gelöscht werden 5 | label_custom_fields_group: Gruppe benutzerdefinierter Felder 6 | label_custom_fields_group_new: Neue Gruppe benutzerdefinierter Felder 7 | label_custom_fields_group_plural: Gruppen für benutzerdefinierte Felder 8 | label_custom_fields_group_settings: Einstellungen für Gruppen mit benutzerdefinierten 9 | Feldern 10 | label_custom_fields_group_tag: Tag für Gruppen von benutzerdefinierten Feldern 11 | label_group_tag_h3: H3 12 | label_group_tag_h4: H4 13 | label_group_tag_fieldset: Eingabefeld 14 | label_fieldset_default_state: Grundeinstellung für Eingabefeld 15 | label_fieldset_state_all_collapsed: Alle zusammengeklappt 16 | label_fieldset_state_all_expended: Alle erweitert 17 | -------------------------------------------------------------------------------- /lib/redmine_custom_fields_groups/patches/user_preference_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineCustomFieldsGroups 2 | module Patches 3 | module UserPreferencePatch 4 | 5 | def self.included(base) # :nodoc: 6 | base.send(:include, InstanceMethods) 7 | 8 | base.class_eval do 9 | safe_attributes 'custom_fields_group_tag', 'fieldset_default_state' 10 | end 11 | end 12 | 13 | module InstanceMethods 14 | def custom_fields_group_tag 15 | self[:custom_fields_group_tag] 16 | end 17 | 18 | def custom_fields_group_tag=(new_value) 19 | self[:custom_fields_group_tag] = new_value 20 | end 21 | 22 | def fieldset_default_state 23 | self[:fieldset_default_state] 24 | end 25 | 26 | def fieldset_default_state=(new_value) 27 | self[:fieldset_default_state] = new_value 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/helpers/custom_fields_groups_helper.rb: -------------------------------------------------------------------------------- 1 | module CustomFieldsGroupsHelper 2 | # Referred: 3 | # - redmine/lib/redmine/field_format.rb 4 | # - def check_box_edit_tag 5 | # - redmine/app/helpers/custom_fields_helper.rb 6 | # - def custom_field_tag_with_label 7 | # - def custom_field_label_tag 8 | def group_fields_edit_tag(group, options={}) 9 | tag_id = 'custom_fields_group[custom_field_ids]' 10 | tag_name = 'custom_fields_group[custom_field_ids][]' 11 | opts = [] 12 | group_field_ids = group.custom_field_ids 13 | other_group_field_ids = CustomFieldsGroupField.all.collect { |gf| 14 | gf.custom_field_id 15 | } - group_field_ids 16 | opts += IssueCustomField.where.not(id: other_group_field_ids).sorted.collect do |cf| 17 | [cf.name, cf.id] 18 | end 19 | s = ''.html_safe 20 | opts.each do |label, value| 21 | value ||= label 22 | checked = group_field_ids.include?(value) 23 | tag = check_box_tag(tag_name, value, checked, :id => tag_id) 24 | s << content_tag('label', tag + ' ' + label) 25 | end 26 | s << hidden_field_tag(tag_name, '', :id => nil) 27 | css = "#{options[:class]} check_box_group" 28 | label = content_tag('label', l(:label_custom_field_plural), :for => tag_id) 29 | label + content_tag('span', s, options.merge(:class => css)) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redmine Custom Fields Groups Plugin 2 | 3 | This is a plugin for grouping custom fields. 4 | 5 | ## Requirements 6 | 7 | - Redmine >= 4.0.0 8 | 9 | ## Installation 10 | 11 | To install Redmine Custom Fields Groups plugin, download or clone this repository in your Redmine installation plugins directory! 12 | 13 | ``` 14 | cd path/to/plugin/directory 15 | git clone https://github.com/gtt-project/redmine_custom_fields_groups.git 16 | ``` 17 | 18 | Then run 19 | 20 | ``` 21 | bundle install 22 | bundle exec rake redmine:plugins:migrate 23 | ``` 24 | 25 | After restarting Redmine, you should be able to see the Redmine Custom Fields Groups plugin in the Plugins page. 26 | 27 | More information on installing (and uninstalling) Redmine plugins can be found here: http://www.redmine.org/wiki/redmine/Plugins 28 | 29 | ## How to use 30 | 31 | TBD 32 | 33 | ## Contributing and Support 34 | 35 | The GTT Project appreciates any [contributions](https://github.com/gtt-project/.github/blob/main/CONTRIBUTING.md)! Feel free to contact us for [reporting problems and support](https://github.com/gtt-project/.github/blob/main/CONTRIBUTING.md). 36 | 37 | ## Version History 38 | 39 | See [all releases](https://github.com/gtt-project/redmine_custom_fields_groups/releases) with release notes. 40 | 41 | ## Authors 42 | 43 | - [Ko Nagase](https://github.com/sanak) 44 | - ... [and others](https://github.com/gtt-project/redmine_custom_fields_groups/graphs/contributors) 45 | 46 | ## LICENSE 47 | 48 | This program is free software. See [LICENSE](LICENSE) for more information. 49 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lib/redmine_custom_fields_groups/hooks/view_layouts_base_html_head_hook' 2 | require_relative 'lib/redmine_custom_fields_groups/hooks/view_user_preferences_hook' 3 | 4 | Redmine::Plugin.register :redmine_custom_fields_groups do 5 | name 'Redmine Custom Fields Groups plugin' 6 | author 'Georepublic' 7 | author_url 'https://github.com/georepublic' 8 | url 'https://github.com/gtt-project/redmine_custom_fields_groups' 9 | description 'This is a plugin for grouping custom fields' 10 | version '1.0.0' 11 | 12 | requires_redmine :version_or_higher => '4.1.0' 13 | 14 | settings partial: 'settings/redmine_custom_fields_groups', 15 | default: { 16 | 'custom_fields_group_tag' => 'h4', 17 | 'fieldset_default_state' => 'all_expended' 18 | } 19 | 20 | menu :admin_menu, 21 | :custom_fields_group, 22 | { controller: 'custom_fields_groups', action: 'index' }, 23 | caption: :label_custom_fields_group_plural, 24 | after: :custom_fields, 25 | html: { class: 'icon icon-custom-fields custom-fields-groups' } 26 | end 27 | 28 | if Rails.version > '6.0' && Rails.autoloaders.zeitwerk_enabled? 29 | require_relative 'app/overrides/issues' 30 | Rails.application.config.after_initialize do 31 | RedmineCustomFieldsGroups.setup 32 | end 33 | else 34 | require 'redmine_custom_fields_groups' 35 | Rails.application.paths["app/overrides"] ||= [] 36 | Rails.application.paths["app/overrides"] << File.expand_path("../app/overrides", __FILE__) 37 | 38 | Rails.configuration.to_prepare do 39 | RedmineCustomFieldsGroups.setup 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/redmine_custom_fields_groups/hooks/view_user_preferences_hook.rb: -------------------------------------------------------------------------------- 1 | module RedmineCustomFieldsGroups 2 | module Hooks 3 | class ViewUserPreferencesHook < Redmine::Hook::ViewListener 4 | def view_users_form_preferences(context={}) 5 | user_custom_fields_group_options(context) 6 | end 7 | 8 | def view_my_account_preferences(context={}) 9 | user_custom_fields_group_options(context) 10 | end 11 | 12 | def user_custom_fields_group_options(context) 13 | user = context[:user] 14 | f = context[:form] 15 | s = '' 16 | 17 | s << "" 18 | s << label_tag("pref_custom_fields_group_tag", l(:label_custom_fields_group_tag)) 19 | s << select_tag( 20 | "pref[custom_fields_group_tag]", 21 | options_for_select([["",""]] + RedmineCustomFieldsGroups::Options::group_tags, user.pref.custom_fields_group_tag), 22 | :id => 'pref_custom_fields_group_tag', 23 | :blank => '' 24 | ) 25 | s << "
" 26 | s << "" 27 | s << label_tag("pref_fieldset_default_state", l(:label_fieldset_default_state)) 28 | s << select_tag( 29 | "pref[fieldset_default_state]", 30 | options_for_select([["",""]] + RedmineCustomFieldsGroups::Options::fieldset_states, user.pref.fieldset_default_state), 31 | :id => 'pref_fieldset_default_state', 32 | :blank => '' 33 | ) 34 | s << "
" 35 | 36 | return s.html_safe 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/custom_fields_groups_controller.rb: -------------------------------------------------------------------------------- 1 | class CustomFieldsGroupsController < ApplicationController 2 | layout 'admin' 3 | 4 | before_action :require_admin 5 | before_action :find_custom_fields_group, only: %i[edit update destroy] 6 | 7 | def index 8 | @custom_fields_groups = CustomFieldsGroup.sorted 9 | end 10 | 11 | def new 12 | @custom_fields_group = CustomFieldsGroup.new 13 | end 14 | 15 | def create 16 | @custom_fields_group = CustomFieldsGroup.new 17 | @custom_fields_group.safe_attributes = custom_fields_group_params 18 | if @custom_fields_group.save 19 | flash[:notice] = l(:notice_successful_create) 20 | redirect_to custom_fields_groups_path 21 | else 22 | render :action => 'new' 23 | end 24 | end 25 | 26 | def edit 27 | end 28 | 29 | def update 30 | @custom_fields_group.safe_attributes = custom_fields_group_params 31 | if @custom_fields_group.save 32 | respond_to do |format| 33 | format.html do 34 | flash[:notice] = l(:notice_successful_update) 35 | redirect_to custom_fields_groups_path 36 | end 37 | format.js { head 200 } 38 | end 39 | else 40 | respond_to do |format| 41 | format.html { render :action => 'edit' } 42 | format.js { head 422 } 43 | end 44 | end 45 | end 46 | 47 | def destroy 48 | begin 49 | if @custom_fields_group.destroy 50 | flash[:notice] = l(:notice_successful_delete) 51 | end 52 | rescue 53 | flash[:error] = l(:error_can_not_delete_custom_fields_group) 54 | end 55 | redirect_to custom_fields_groups_path 56 | end 57 | 58 | private 59 | 60 | def custom_fields_group_params 61 | params.require(:custom_fields_group).permit(:name, :position, custom_field_ids: []) 62 | end 63 | 64 | def find_custom_fields_group 65 | @custom_fields_group = CustomFieldsGroup.find(params[:id]) 66 | rescue ActiveRecord::RecordNotFound 67 | render_404 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/unit/custom_fields_group_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class CustomFieldsGroupTest < ActiveSupport::TestCase 4 | fixtures :custom_fields, :custom_fields_groups, :custom_fields_group_fields 5 | 6 | test 'create' do 7 | issue_custom_field = IssueCustomField.new(:name => 'test', :field_format => 'text') 8 | issue_custom_field.save! 9 | issue_custom_field.reload 10 | 11 | custom_fields_group = CustomFieldsGroup.new 12 | custom_fields_group.name = 'test' 13 | custom_fields_group.custom_field_ids = [issue_custom_field.id] 14 | assert custom_fields_group.save 15 | custom_fields_group.reload 16 | assert_equal [issue_custom_field.id], custom_fields_group.custom_field_ids 17 | assert_equal CustomFieldsGroup.count, custom_fields_group.position 18 | end 19 | 20 | test 'should require name' do 21 | custom_fields_group = CustomFieldsGroup.new 22 | assert_not custom_fields_group.save 23 | assert custom_fields_group.errors[:name] 24 | end 25 | 26 | test 'should validate name uniqueness' do 27 | assert_difference 'CustomFieldsGroup.count' do 28 | custom_fields_group = CustomFieldsGroup.new 29 | custom_fields_group.name = 'test' 30 | assert custom_fields_group.save 31 | assert_equal 'test', custom_fields_group.name 32 | end 33 | 34 | assert_no_difference 'CustomFieldsGroup.count' do 35 | custom_fields_group = CustomFieldsGroup.new 36 | custom_fields_group.name = 'test' 37 | assert_not custom_fields_group.save 38 | assert custom_fields_group.errors[:name] 39 | end 40 | end 41 | 42 | test 'deletion of custom_field_group should delete custom_fields_group_field' do 43 | custom_fields_group = custom_fields_groups(:custom_fields_groups_100) 44 | assert_difference 'CustomFieldsGroupField.count', -2 do 45 | assert custom_fields_group.destroy 46 | end 47 | end 48 | 49 | test 'deletion of custom_field should delete custom_fields_group_field' do 50 | custom_field = custom_fields(:custom_fields_001) 51 | assert_difference 'CustomFieldsGroup.find(1).custom_fields.count', -1 do 52 | assert custom_field.destroy 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | env: 4 | PLUGIN_NAME: ${{ github.event.repository.name }} 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - next 11 | pull_request: 12 | branches: 13 | - main 14 | - next 15 | workflow_dispatch: 16 | 17 | jobs: 18 | test: 19 | name: redmine:${{ matrix.redmine_version }} ruby:${{ matrix.ruby_version }} db:${{ matrix.db }} 20 | runs-on: ubuntu-22.04 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | redmine_version: [4.2-stable, 5.0-stable, 5.1-stable, master] 26 | ruby_version: ['2.7', '3.0', '3.1', '3.2'] 27 | db: ['mysql:5.7', 'postgres:10', 'sqlite3'] 28 | # System test takes 2~3 times longer, so limit to specific matrix combinations 29 | # See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#expanding-or-adding-matrix-configurations 30 | include: 31 | - system_test: true 32 | redmine_version: 5.1-stable 33 | ruby_version: '3.2' 34 | db: 'mysql:5.7' 35 | exclude: 36 | - redmine_version: 4.2-stable 37 | ruby_version: '3.0' 38 | - redmine_version: 4.2-stable 39 | ruby_version: '3.1' 40 | - redmine_version: 4.2-stable 41 | ruby_version: '3.2' 42 | - redmine_version: 5.0-stable 43 | ruby_version: '3.2' 44 | - redmine_version: master 45 | ruby_version: '2.7' 46 | 47 | steps: 48 | - name: Setup Redmine 49 | uses: hidakatsuya/action-setup-redmine@v1 50 | with: 51 | repository: redmine/redmine 52 | version: ${{ matrix.redmine_version }} 53 | ruby-version: ${{ matrix.ruby_version }} 54 | database: ${{ matrix.db }} 55 | path: redmine 56 | 57 | - name: Checkout Plugin 58 | uses: actions/checkout@v4 59 | with: 60 | path: redmine/plugins/${{ env.PLUGIN_NAME }} 61 | 62 | - name: Install Ruby dependencies 63 | working-directory: redmine 64 | run: | 65 | bundle config set --local without 'development' 66 | bundle install --jobs=4 --retry=3 67 | 68 | - name: Run Redmine rake tasks 69 | working-directory: redmine 70 | run: | 71 | bundle exec rake generate_secret_token 72 | bundle exec rake db:create db:migrate redmine:plugins:migrate 73 | 74 | - name: Zeitwerk check 75 | working-directory: redmine 76 | run: | 77 | if grep -q zeitwerk config/application.rb ; then 78 | bundle exec rake zeitwerk:check 79 | fi 80 | shell: bash 81 | 82 | - name: Run plugin tests 83 | working-directory: redmine 84 | run: | 85 | bundle exec rake redmine:plugins:test:units NAME=${{ env.PLUGIN_NAME }} RUBYOPT="-W0" 86 | bundle exec rake redmine:plugins:test:functionals NAME=${{ env.PLUGIN_NAME }} RUBYOPT="-W0" 87 | bundle exec rake redmine:plugins:test:integration NAME=${{ env.PLUGIN_NAME }} RUBYOPT="-W0" 88 | if [ ${{ matrix.system_test }} = "true" ]; then 89 | bundle exec rake redmine:plugins:test:system NAME=${{ env.PLUGIN_NAME }} RUBYOPT="-W0" 90 | fi 91 | 92 | # - name: Run core tests 93 | # env: 94 | # RAILS_ENV: test 95 | # PARALLEL_WORKERS: 1 96 | # working-directory: redmine 97 | # run: bundle exec rake test 98 | 99 | # - name: Run core system tests 100 | # if: matrix.system_test == true 101 | # env: 102 | # RAILS_ENV: test 103 | # GOOGLE_CHROME_OPTS_ARGS: "headless,disable-gpu,no-sandbox,disable-dev-shm-usage" 104 | # working-directory: redmine 105 | # run: bundle exec rake test:system 106 | 107 | - name: Run uninstall test 108 | working-directory: redmine 109 | run: bundle exec rake redmine:plugins:migrate NAME=${{ env.PLUGIN_NAME }} VERSION=0 110 | -------------------------------------------------------------------------------- /test/system/fieldset_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../../test/application_system_test_case' 2 | require_relative '../test_helper' 3 | 4 | class FieldsetTest < ApplicationSystemTestCase 5 | fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, 6 | :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, 7 | :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, 8 | :watchers, :journals, :journal_details, 9 | :custom_fields, :custom_fields_groups, :custom_fields_group_fields 10 | 11 | teardown do 12 | Setting.where(name: 'plugin_redmine_custom_fields_groups').destroy_all 13 | Setting.clear_cache 14 | end 15 | 16 | test 'click group title should collapse/expand fieldset with default state all_expended' do 17 | Setting.plugin_redmine_custom_fields_groups = { 18 | 'custom_fields_group_tag' => 'fieldset', 19 | 'fieldset_default_state' => 'all_expended' 20 | } 21 | 22 | log_user('dlopper', 'foo') 23 | visit '/issues/1' 24 | 25 | # issue details 26 | within('div.issue.details div.attributes') do 27 | assert page.has_content?('Group 1') 28 | # default expanded 29 | assert page.has_content?('Searchable field') 30 | assert page.has_content?('Database') 31 | # click group title 32 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 33 | # collapsed fields 34 | assert page.has_no_content?('Searchable field') 35 | assert page.has_no_content?('Database') 36 | # click group title, again 37 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 38 | # expanded fields 39 | assert page.has_content?('Searchable field') 40 | assert page.has_content?('Database') 41 | end 42 | 43 | # issue edit 44 | page.first(:link, 'Edit').click 45 | page.find('#issue_notes:focus') 46 | sleep 0.1 47 | within('div#update div.attributes') do 48 | assert page.has_content?('Group 1') 49 | # default expanded 50 | assert page.has_content?('Searchable field') 51 | assert page.has_content?('Database') 52 | # click group title 53 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 54 | # collapsed fields 55 | assert page.has_no_content?('Searchable field') 56 | assert page.has_no_content?('Database') 57 | # click group title, again 58 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 59 | # expanded fields 60 | assert page.has_content?('Searchable field') 61 | assert page.has_content?('Database') 62 | end 63 | end 64 | 65 | test 'click group title should expand/collapse fieldset with default state all_collapsed' do 66 | Setting.plugin_redmine_custom_fields_groups = { 67 | 'custom_fields_group_tag' => 'fieldset', 68 | 'fieldset_default_state' => 'all_collapsed' 69 | } 70 | 71 | log_user('dlopper', 'foo') 72 | visit '/issues/1' 73 | 74 | # issue details 75 | within('div.issue.details div.attributes') do 76 | assert page.has_content?('Group 1') 77 | # default collapsed 78 | assert page.has_no_content?('Searchable field') 79 | assert page.has_no_content?('Database') 80 | # click group title 81 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 82 | # expanded fields 83 | assert page.has_content?('Searchable field') 84 | assert page.has_content?('Database') 85 | # click group title, again 86 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 87 | # collapsed fields 88 | assert page.has_no_content?('Searchable field') 89 | assert page.has_no_content?('Database') 90 | end 91 | 92 | # issue edit 93 | page.first(:link, 'Edit').click 94 | page.find('#issue_notes:focus') 95 | sleep 0.1 96 | within('div#update div.attributes') do 97 | assert page.has_content?('Group 1') 98 | # default collapsed 99 | assert page.has_no_content?('Searchable field') 100 | assert page.has_no_content?('Database') 101 | # click group title 102 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 103 | # expanded fields 104 | assert page.has_content?('Searchable field') 105 | assert page.has_content?('Database') 106 | # click group title, again 107 | find('fieldset.custom-fields-groups > legend.icon', :text => 'Group 1').click 108 | # collapsed fields 109 | assert page.has_no_content?('Searchable field') 110 | assert page.has_no_content?('Database') 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /app/views/issues/_form_custom_fields.html.erb: -------------------------------------------------------------------------------- 1 | <% custom_fields_group_tag = User.current.pref.custom_fields_group_tag %> 2 | <% if custom_fields_group_tag.blank? %> 3 | <% custom_fields_group_tag = Setting.plugin_redmine_custom_fields_groups['custom_fields_group_tag'] || 'h4' %> 4 | <% end %> 5 | <% fieldset_default_state = User.current.pref.fieldset_default_state %> 6 | <% if fieldset_default_state.blank? %> 7 | <% fieldset_default_state = Setting.plugin_redmine_custom_fields_groups['fieldset_default_state'] || 'all_expended' %> 8 | <% end %> 9 | <% all_collapsed = (fieldset_default_state == 'all_collapsed') %> 10 | <% grouped_custom_field_values(@issue.editable_custom_field_values).each do |title, values| %> 11 | <% if values.present? %> 12 | <% full_width_values = values.select { |value| value.custom_field.full_width_layout? } %> 13 | <% half_width_values = values - full_width_values %> 14 | <% if custom_fields_group_tag == 'fieldset' %> 15 | <% if title.nil? %> 16 | <% if half_width_values.present? %> 17 |<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
23 | <% if i == split_on -%> 24 |<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
34 | <%= wikitoolbar_for "issue_custom_field_values_#{value.custom_field_id}", preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) if value.custom_field.full_text_formatting? %> 35 | <% end %> 36 | <% else %> 37 | <% fieldset_class = 'collapsible custom-fields-groups' + (all_collapsed ? ' collapsed' : '') %> 38 | <% legend_class = 'icon icon-' + (all_collapsed ? 'collapsed' : ((Redmine::VERSION.to_s >= '5.0.0') ? 'expanded' : 'expended')) %> 39 | <% div_style = all_collapsed ? 'display: none' : '' %> 40 | 63 | <% end %> 64 | <% else %> 65 | <%= content_tag(custom_fields_group_tag, title, :class => "custom-fields-groups") unless title.nil? %> 66 | <% if half_width_values.present? %> 67 |<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
73 | <% if i == split_on -%> 74 |<%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %>
84 | <%= wikitoolbar_for "issue_custom_field_values_#{value.custom_field_id}", preview_issue_path(:project_id => @issue.project, :issue_id => @issue.id) if value.custom_field.full_text_formatting? %> 85 | <% end %> 86 | <% end %> 87 | <% end %> 88 | <% end %> 89 | -------------------------------------------------------------------------------- /test/functional/custom_fields_groups_controller_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class CustomFieldsGroupsControllerTest < ActionController::TestCase 4 | fixtures :custom_fields, :custom_fields_groups, :custom_fields_group_fields, 5 | :users 6 | 7 | setup do 8 | User.current = nil 9 | @request.session[:user_id] = 1 # admin 10 | end 11 | 12 | teardown do 13 | @request.session.clear 14 | end 15 | 16 | test 'should require admin' do 17 | @request.session[:user_id] = nil 18 | get :index 19 | assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fcustom_fields_groups' 20 | end 21 | 22 | test 'should get index' do 23 | get :index 24 | assert_response :success 25 | 26 | assert_select 'table.custom_fields_groups tbody' do 27 | assert_select 'tr', CustomFieldsGroup.count 28 | assert_select 'a[href="/custom_fields_groups/1/edit"]', :text => 'Group 1' 29 | end 30 | end 31 | 32 | test 'should get new' do 33 | get :new 34 | assert_response :success 35 | assert_select 'input[type=text][name=?]', 'custom_fields_group[name]' 36 | assert_select 'input[type=checkbox][name=?][value=?]', 'custom_fields_group[custom_field_ids][]', '9' 37 | end 38 | 39 | test 'should create custom fields gruop' do 40 | assert_difference 'CustomFieldsGroup.count' do 41 | post :create, :params => { 42 | :custom_fields_group => { 43 | :name => 'Group 4', 44 | :custom_field_ids => [9] 45 | } 46 | } 47 | end 48 | assert_redirected_to '/custom_fields_groups' 49 | 50 | assert custom_fields_group = CustomFieldsGroup.find_by_name('Group 4') 51 | assert_equal [9], custom_fields_group.custom_field_ids 52 | assert_equal 4, custom_fields_group.position 53 | end 54 | 55 | test 'should not create custom fields gruop without name' do 56 | post :create, :params => { 57 | :custom_fields_group => { 58 | :name => '', 59 | :custom_field_ids => [9] 60 | } 61 | } 62 | assert_response :success 63 | assert_select_error /Name cannot be blank/ 64 | end 65 | 66 | test 'should get edit' do 67 | get :edit, :params => { :id => 1 } 68 | assert_response :success 69 | 70 | assert_select 'input[type=text][name=?][value=?]', 'custom_fields_group[name]', 'Group 1' 71 | assert_select 'input[type=checkbox][name=?][value=?][checked=?]', 'custom_fields_group[custom_field_ids][]', '1', 'checked' 72 | assert_select 'input[type=checkbox][name=?][value=?][checked=?]', 'custom_fields_group[custom_field_ids][]', '2', 'checked' 73 | end 74 | 75 | test 'should update custom fields group' do 76 | post :update, :params => { 77 | :id => 1, 78 | :custom_fields_group => { 79 | :name => 'Group 1 updated', 80 | :custom_field_ids => [2] 81 | } 82 | } 83 | assert_redirected_to '/custom_fields_groups' 84 | 85 | assert custom_fields_group = CustomFieldsGroup.find(1) 86 | assert_equal 'Group 1 updated', custom_fields_group.name 87 | assert_equal [2], custom_fields_group.custom_field_ids 88 | end 89 | 90 | test 'should not update custom fields group without name' do 91 | post :update, :params => { 92 | :id => 1, 93 | :custom_fields_group => { 94 | :name => '', 95 | :custom_field_ids => [2] 96 | } 97 | } 98 | assert_response :success 99 | assert_select_error /Name cannot be blank/ 100 | end 101 | 102 | test 'should destroy custom fields group' do 103 | assert_difference 'CustomFieldsGroup.count', -1 do 104 | delete :destroy, :params => { :id => 1 } 105 | end 106 | assert_redirected_to '/custom_fields_groups' 107 | end 108 | 109 | test 'move highest' do 110 | put :update, :params => { 111 | :id => 2, 112 | :custom_fields_group => { 113 | :position => 1 114 | } 115 | }, :xhr => true 116 | assert_response :success 117 | assert_equal 1, CustomFieldsGroup.find(2).position 118 | end 119 | 120 | test 'move higher' do 121 | position = CustomFieldsGroup.find(2).position 122 | put :update, :params => { 123 | :id => 2, 124 | :custom_fields_group => { 125 | :position => position - 1 126 | } 127 | }, :xhr => true 128 | assert_response :success 129 | assert_equal position - 1, CustomFieldsGroup.find(2).position 130 | end 131 | 132 | test 'move lower' do 133 | position = CustomFieldsGroup.find(2).position 134 | put :update, :params => { 135 | :id => 2, 136 | :custom_fields_group => { 137 | :position => position + 1 138 | } 139 | }, :xhr => true 140 | assert_response :success 141 | assert_equal position + 1, CustomFieldsGroup.find(2).position 142 | end 143 | 144 | test 'move lowest' do 145 | put :update, :params => { 146 | :id => 2, 147 | :custom_fields_group => { 148 | :position => CustomFieldsGroup.count 149 | } 150 | }, :xhr => true 151 | assert_response :success 152 | assert_equal CustomFieldsGroup.count, CustomFieldsGroup.find(2).position 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/redmine_custom_fields_groups/patches/issues_helper_patch.rb: -------------------------------------------------------------------------------- 1 | module RedmineCustomFieldsGroups 2 | module Patches 3 | module IssuesHelperPatch 4 | 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.send(:prepend, InstanceMethods) 8 | base.class_eval do 9 | 10 | # Referred: 11 | # - Patch #30919: Group Issues Custom Fields - (Form like Issues) - Redmine 12 | # - https://www.redmine.org/issues/30919 13 | def grouped_custom_field_values(custom_field_values) 14 | keys_grouped = CustomFieldsGroupField. 15 | joins(:custom_fields_group, :custom_field). 16 | order('custom_fields_groups.position', 'custom_fields.position'). 17 | pluck('custom_fields_groups.name', :custom_field_id).group_by(&:shift) 18 | custom_fields_grouped = { nil => (keys_grouped[nil].nil? ? [] : 19 | keys_grouped[nil].map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten) | 20 | custom_field_values.select{|y| ! keys_grouped.values.flatten.include?(y.custom_field[:id])}} 21 | keys_grouped.reject{|k,v| k == nil}.each{|k,v| custom_fields_grouped[k] = 22 | v.map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten} 23 | custom_fields_grouped 24 | end 25 | 26 | def render_custom_fields_rows_by_groups(issue) 27 | custom_field_values = issue.visible_custom_field_values 28 | return if custom_field_values.empty? 29 | 30 | custom_fields_group_tag = User.current.pref.custom_fields_group_tag 31 | if custom_fields_group_tag.blank? 32 | custom_fields_group_tag = Setting.plugin_redmine_custom_fields_groups['custom_fields_group_tag'] || 'h4' 33 | end 34 | fieldset_default_state = User.current.pref.fieldset_default_state 35 | if fieldset_default_state.blank? 36 | fieldset_default_state = Setting.plugin_redmine_custom_fields_groups['fieldset_default_state'] || 'all_expended' 37 | end 38 | 39 | s = ''.html_safe 40 | grouped_custom_field_values(custom_field_values).each do |title, values| 41 | if values.present? 42 | if custom_fields_group_tag == 'fieldset' 43 | if title.nil? 44 | s << render_half_width_custom_fields_rows_by_grouped_values(issue, values) 45 | s << render_full_width_custom_fields_rows_by_grouped_values(issue, values) 46 | else 47 | s << content_tag('fieldset', :class => 'collapsible custom-fields-groups') do 48 | concat content_tag('legend', title, :onclick => 'toggleFieldset(this);', 49 | :class => 'icon icon-' + ((Redmine::VERSION.to_s >= '5.0.0') ? 'expanded' : 'expended')) 50 | concat render_half_width_custom_fields_rows_by_grouped_values(issue, values) 51 | concat render_full_width_custom_fields_rows_by_grouped_values(issue, values) 52 | end 53 | end 54 | else 55 | s << content_tag(custom_fields_group_tag, title, :class => 'custom-fields-groups') unless title.nil? 56 | s << render_half_width_custom_fields_rows_by_grouped_values(issue, values) 57 | s << render_full_width_custom_fields_rows_by_grouped_values(issue, values) 58 | end 59 | end 60 | end 61 | # temporary hack 62 | if custom_fields_group_tag == 'fieldset' && fieldset_default_state == 'all_collapsed' 63 | s << javascript_tag("$('div.issue div.attributes fieldset.custom-fields-groups>legend').each(function(idx,elem){toggleFieldset(elem);})") 64 | end 65 | s 66 | end 67 | 68 | def render_half_width_custom_fields_rows_by_grouped_values(issue, custom_field_values) 69 | values = custom_field_values.reject {|value| value.custom_field.full_width_layout?} 70 | return if values.empty? 71 | 72 | half = (values.size / 2.0).ceil 73 | issue_fields_rows do |rows| 74 | values.each_with_index do |value, i| 75 | m = (i < half ? :left : :right) 76 | rows.send m, custom_field_name_tag(value.custom_field), custom_field_value_tag(value), :class => value.custom_field.css_classes 77 | end 78 | end 79 | end 80 | 81 | def render_full_width_custom_fields_rows_by_grouped_values(issue, custom_field_values) 82 | values = custom_field_values.select {|value| value.custom_field.full_width_layout?} 83 | return if values.empty? 84 | 85 | s = ''.html_safe 86 | values.each_with_index do |value, i| 87 | # attr_value_tag = custom_field_value_tag(value) 88 | # next if attr_value_tag.blank? 89 | 90 | # content = 91 | # content_tag('hr') + 92 | # content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) + 93 | # content_tag('div', attr_value_tag, class: 'value') 94 | # s << content_tag('div', content, class: "#{value.custom_field.css_classes} attribute") 95 | content = content_tag('div', custom_field_name_tag(value.custom_field) + ":", :class => 'label') + 96 | content_tag('div', custom_field_value_tag(value), :class => 'value') 97 | content = content_tag('div', content, :class => "#{value.custom_field.css_classes} attribute") 98 | s << content_tag('div', content, :class => 'splitcontent') 99 | end 100 | s 101 | end 102 | end #base 103 | end #self 104 | 105 | module InstanceMethods 106 | 107 | end #module 108 | 109 | module ClassMethods 110 | 111 | end #module 112 | end #module 113 | end #module 114 | end #module 115 | -------------------------------------------------------------------------------- /test/integration/layout_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class LayoutTest < Redmine::IntegrationTest 4 | fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, 5 | :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues, 6 | :enumerations, :custom_fields, :custom_values, :custom_fields_trackers, 7 | :watchers, :journals, :journal_details, :versions, 8 | :workflows, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, 9 | :custom_fields, :custom_fields_groups, :custom_fields_group_fields 10 | 11 | setup do 12 | User.current = nil 13 | @user = User.find_by_login('dlopper') 14 | end 15 | 16 | teardown do 17 | Setting.where(name: 'plugin_redmine_custom_fields_groups').destroy_all 18 | Setting.clear_cache 19 | @user.pref.others.delete(:custom_fields_group_tag) 20 | @user.pref.others.delete(:fieldset_default_state) 21 | end 22 | 23 | test 'should show custom fields groups with default h4 tag in issue' do 24 | log_user('dlopper', 'foo') 25 | get '/issues/1' 26 | assert_response :success 27 | 28 | # issue details 29 | assert_select 'div.issue.details div.attributes h4.custom-fields-groups:contains("Group 1") + div.splitcontent' do 30 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 31 | assert_select 'span', :text => 'Searchable field' 32 | assert_select 'div.value', :text => '125' 33 | end 34 | assert_select 'div.splitcontentleft:nth-of-type(2)' do 35 | assert_select 'span', :text => 'Database' 36 | assert_select 'div.value', :text => '' 37 | end 38 | end 39 | assert_select 'div.issue.details div.attributes h4.custom-fields-groups:contains("Group 2") + div.splitcontent' do 40 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 41 | assert_select 'span', :text => 'Float field' 42 | assert_select 'div.value', :text => '2.10' 43 | end 44 | end 45 | assert_select 'div.issue.details div.attributes h4.custom-fields-groups:contains("Group 3") + div.splitcontent' do 46 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 47 | assert_select 'span', :text => 'Custom date' 48 | assert_select 'div.value', :text => '12/01/2009' 49 | end 50 | end 51 | 52 | # issue edit 53 | assert_select 'div#update div.attributes h4.custom-fields-groups:contains("Group 1") + div.splitcontent' do 54 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 55 | assert_select 'span', :text => 'Searchable field' 56 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][2]', :value => '125' 57 | end 58 | assert_select 'div.splitcontentright:nth-of-type(1)' do 59 | assert_select 'span', :text => 'Database' 60 | assert_select 'select[name=?]', 'issue[custom_field_values][1]', :value => '' 61 | end 62 | end 63 | assert_select 'div#update div.attributes h4.custom-fields-groups:contains("Group 2") + div.splitcontent' do 64 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 65 | assert_select 'span', :text => 'Float field' 66 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][6]', :value => '2.1' 67 | end 68 | end 69 | assert_select 'div#update div.attributes h4.custom-fields-groups:contains("Group 3") + div.splitcontent' do 70 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 71 | assert_select 'span', :text => 'Custom date' 72 | assert_select 'input[type=date][name=?]', 'issue[custom_field_values][8]', :value => '2009-12-01' 73 | end 74 | end 75 | end 76 | 77 | test 'should show custom fields groups with h3 tag in issue from plugin setting' do 78 | Setting.plugin_redmine_custom_fields_groups = { 'custom_fields_group_tag' => 'h3' } 79 | 80 | log_user('dlopper', 'foo') 81 | get '/issues/1' 82 | assert_response :success 83 | 84 | # issue details 85 | assert_select 'div.issue.details div.attributes h3.custom-fields-groups:contains("Group 1") + div.splitcontent' do 86 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 87 | assert_select 'span', :text => 'Searchable field' 88 | assert_select 'div.value', :text => '125' 89 | end 90 | assert_select 'div.splitcontentleft:nth-of-type(2)' do 91 | assert_select 'span', :text => 'Database' 92 | assert_select 'div.value', :text => '' 93 | end 94 | end 95 | assert_select 'div.issue.details div.attributes h3.custom-fields-groups:contains("Group 2") + div.splitcontent' do 96 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 97 | assert_select 'span', :text => 'Float field' 98 | assert_select 'div.value', :text => '2.10' 99 | end 100 | end 101 | assert_select 'div.issue.details div.attributes h3.custom-fields-groups:contains("Group 3") + div.splitcontent' do 102 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 103 | assert_select 'span', :text => 'Custom date' 104 | assert_select 'div.value', :text => '12/01/2009' 105 | end 106 | end 107 | 108 | # issue edit 109 | assert_select 'div#update div.attributes h3.custom-fields-groups:contains("Group 1") + div.splitcontent' do 110 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 111 | assert_select 'span', :text => 'Searchable field' 112 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][2]', :value => '125' 113 | end 114 | assert_select 'div.splitcontentright:nth-of-type(1)' do 115 | assert_select 'span', :text => 'Database' 116 | assert_select 'select[name=?]', 'issue[custom_field_values][1]', :value => '' 117 | end 118 | end 119 | assert_select 'div#update div.attributes h3.custom-fields-groups:contains("Group 2") + div.splitcontent' do 120 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 121 | assert_select 'span', :text => 'Float field' 122 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][6]', :value => '2.1' 123 | end 124 | end 125 | assert_select 'div#update div.attributes h3.custom-fields-groups:contains("Group 3") + div.splitcontent' do 126 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 127 | assert_select 'span', :text => 'Custom date' 128 | assert_select 'input[type=date][name=?]', 'issue[custom_field_values][8]', :value => '2009-12-01' 129 | end 130 | end 131 | end 132 | 133 | test 'should show custom fields groups with fieldset tag all expended in issue from plugin setting' do 134 | Setting.plugin_redmine_custom_fields_groups = { 135 | 'custom_fields_group_tag' => 'fieldset', 136 | 'fieldset_default_state' => 'all_expended' 137 | } 138 | 139 | log_user('dlopper', 'foo') 140 | get '/issues/1' 141 | assert_response :success 142 | 143 | # issue details 144 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 1") + div.splitcontent' do 145 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 146 | assert_select 'span', :text => 'Searchable field' 147 | assert_select 'div.value', :text => '125' 148 | end 149 | assert_select 'div.splitcontentleft:nth-of-type(2)' do 150 | assert_select 'span', :text => 'Database' 151 | assert_select 'div.value', :text => '' 152 | end 153 | end 154 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 2") + div.splitcontent' do 155 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 156 | assert_select 'span', :text => 'Float field' 157 | assert_select 'div.value', :text => '2.10' 158 | end 159 | end 160 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 3") + div.splitcontent' do 161 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 162 | assert_select 'span', :text => 'Custom date' 163 | assert_select 'div.value', :text => '12/01/2009' 164 | end 165 | end 166 | 167 | # issue edit 168 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 1") + div.splitcontent' do 169 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 170 | assert_select 'span', :text => 'Searchable field' 171 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][2]', :value => '125' 172 | end 173 | assert_select 'div.splitcontentright:nth-of-type(1)' do 174 | assert_select 'span', :text => 'Database' 175 | assert_select 'select[name=?]', 'issue[custom_field_values][1]', :value => '' 176 | end 177 | end 178 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 2") + div.splitcontent' do 179 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 180 | assert_select 'span', :text => 'Float field' 181 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][6]', :value => '2.1' 182 | end 183 | end 184 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 3") + div.splitcontent' do 185 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 186 | assert_select 'span', :text => 'Custom date' 187 | assert_select 'input[type=date][name=?]', 'issue[custom_field_values][8]', :value => '2009-12-01' 188 | end 189 | end 190 | end 191 | 192 | test 'should override user preference setting than plugin setting' do 193 | Setting.plugin_redmine_custom_fields_groups = { 194 | 'custom_fields_group_tag' => 'h4', # default 195 | 'fieldset_default_state' => 'all_collapsed' 196 | } 197 | 198 | @user.pref.others[:custom_fields_group_tag] = 'fieldset' 199 | @user.pref.others[:fieldset_default_state] = 'all_expended' 200 | @user.pref.save! 201 | 202 | log_user('dlopper', 'foo') 203 | get '/issues/1' 204 | assert_response :success 205 | 206 | # issue details 207 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 1") + div.splitcontent' do 208 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 209 | assert_select 'span', :text => 'Searchable field' 210 | assert_select 'div.value', :text => '125' 211 | end 212 | assert_select 'div.splitcontentleft:nth-of-type(2)' do 213 | assert_select 'span', :text => 'Database' 214 | assert_select 'div.value', :text => '' 215 | end 216 | end 217 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 2") + div.splitcontent' do 218 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 219 | assert_select 'span', :text => 'Float field' 220 | assert_select 'div.value', :text => '2.10' 221 | end 222 | end 223 | assert_select 'div.issue.details div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 3") + div.splitcontent' do 224 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 225 | assert_select 'span', :text => 'Custom date' 226 | assert_select 'div.value', :text => '12/01/2009' 227 | end 228 | end 229 | 230 | # issue edit 231 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 1") + div.splitcontent' do 232 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 233 | assert_select 'span', :text => 'Searchable field' 234 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][2]', :value => '125' 235 | end 236 | assert_select 'div.splitcontentright:nth-of-type(1)' do 237 | assert_select 'span', :text => 'Database' 238 | assert_select 'select[name=?]', 'issue[custom_field_values][1]', :value => '' 239 | end 240 | end 241 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 2") + div.splitcontent' do 242 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 243 | assert_select 'span', :text => 'Float field' 244 | assert_select 'input[type=text][name=?]', 'issue[custom_field_values][6]', :value => '2.1' 245 | end 246 | end 247 | assert_select 'div#update div.attributes fieldset.custom-fields-groups > legend.icon:contains("Group 3") + div.splitcontent' do 248 | assert_select 'div.splitcontentleft:nth-of-type(1)' do 249 | assert_select 'span', :text => 'Custom date' 250 | assert_select 'input[type=date][name=?]', 'issue[custom_field_values][8]', :value => '2009-12-01' 251 | end 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc.