Project

General

Profile

Patch #30919 » GCF-4.0.patch

Frederico Camara, 2019-02-25 17:05

View differences:

app/controllers/projects_controller.rb
177 177
    @version_status = params[:version_status] || 'open'
178 178
    @version_name = params[:version_name]
179 179
    @versions = @project.shared_versions.status(@version_status).like(@version_name).sorted
180
    @cfs=AttributeGroupField.joins(:attribute_group).joins(:custom_field).joins(:tracker).
181
       where(:attribute_groups => {project_id: @project}, :custom_fields => {id: @project.all_issue_custom_fields.pluck(:id)}).
182
       pluck("trackers.id", "attribute_groups.id", "attribute_groups.name", "attribute_groups.position",
183
             "id", "position", "custom_fields.id", "custom_fields.name", "custom_fields.position").sort_by{|x| [x[3], x[5]]}
180 184
  end
181 185

  
182 186
  def edit
......
203 207
    end
204 208
  end
205 209

  
210
  def groupissuescustomfields
211
    # clean invalid values: invalid cfs, empty cf lists, empty groups
212
    group_issues_custom_fields = (JSON.parse params[:group_issues_custom_fields]).
213
      each{|tid,v| v.replace(v.select{|k,v| v["cfs"] ? v["cfs"].delete_if{|k,v| @project.all_issue_custom_fields.pluck(:id).include?(v)} : v})}.
214
      each{|tid,v| v.delete_if{|k,v| v["cfs"].blank?}}.
215
      delete_if{|k,v| v.blank?}
216

  
217
    groups = AttributeGroup.where(project_id: @project.id).collect(&:id)
218
    fields = AttributeGroupField.where(attribute_group_id: groups).collect(&:id) 
219
    group_issues_custom_fields.each do |tid,v|
220
      v.each do |gp, g|
221
        gid = groups.shift
222
        if gid.nil?
223
          gid=AttributeGroup.create(project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp).id
224
        else
225
          AttributeGroup.update(gid, project_id: @project.id, tracker_id: tid, name: g["name"].nil? ? nil : g["name"], position: gp)
226
        end
227
        g['cfs'].each do |cfp, cf|
228
          cfid = fields.shift
229
          if cfid.nil?
230
            AttributeGroupField.create(attribute_group_id: gid, custom_field_id: cf, position: cfp)
231
          else
232
            AttributeGroupField.update(cfid, attribute_group_id: gid, custom_field_id: cf, position: cfp)
233
          end
234
        end
235
      end
236
    end
237
    fields.each do |i|
238
      AttributeGroupField.delete(i)
239
    end
240
    groups.each do |i|
241
      AttributeGroup.delete(i)
242
    end
243
    flash[:notice] = l(:notice_successful_update)
244
    redirect_to settings_project_path(@project, :tab => 'groupissuescustomfields')
245
  end
246

  
206 247
  def archive
207 248
    unless @project.archive
208 249
      flash[:error] = l(:error_can_not_archive_project)
app/helpers/issues_helper.rb
227 227
    r.to_html
228 228
  end
229 229

  
230
  def render_half_width_custom_fields_rows(issue)
231
    values = issue.visible_custom_field_values.reject {|value| value.custom_field.full_width_layout?}
232
    return if values.empty?
233
    half = (values.size / 2.0).ceil
234
    issue_fields_rows do |rows|
235
      values.each_with_index do |value, i|
236
        css = "cf_#{value.custom_field.id}"
237
        attr_value = show_value(value)
238
        if value.custom_field.text_formatting == 'full'
239
          attr_value = content_tag('div', attr_value, class: 'wiki')
240
        end
241
        m = (i < half ? :left : :right)
242
        rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => css
243
      end
244
    end
230
  def group_by_keys(project_id, tracker_id, custom_field_values)
231
    keys_grouped = AttributeGroupField.joins(:attribute_group).
232
      where(:attribute_groups => {project_id: project_id, tracker_id: tracker_id}).
233
      order("attribute_groups.position", :position).pluck(:name, :custom_field_id).group_by(&:shift)
234
    custom_fields_grouped = { nil => (keys_grouped[nil].nil? ? [] : keys_grouped[nil].map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten) |
235
      custom_field_values.select{|y| ! keys_grouped.values.flatten.include?(y.custom_field[:id])}}
236
    keys_grouped.reject{|k,v| k == nil}.each{|k,v| custom_fields_grouped[k] = v.map{|n| custom_field_values.select{|x| x.custom_field[:id] == n[0]}}.flatten}
237
    custom_fields_grouped
245 238
  end
246 239

  
247
  def render_full_width_custom_fields_rows(issue)
248
    values = issue.visible_custom_field_values.select {|value| value.custom_field.full_width_layout?}
249
    return if values.empty?
250

  
240
  def render_custom_fields_rows(issue)
251 241
    s = ''.html_safe
252
    values.each_with_index do |value, i|
253
      attr_value = show_value(value)
254
      next if attr_value.blank?
255

  
256
      if value.custom_field.text_formatting == 'full'
257
        attr_value = content_tag('div', attr_value, class: 'wiki')
242
    group_by_keys(issue.project_id, issue.tracker_id, issue.visible_custom_field_values).each do |title, values|
243
      if values.present?
244
        s += content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil?
245
        while values.present?
246
          if values[0].custom_field.full_width_layout?
247
            while values.present? && values[0].custom_field.full_width_layout?
248
              value=values.shift
249
              attr_value = show_value(value)
250
              if value.custom_field.text_formatting == 'full'
251
                attr_value = content_tag('div', attr_value, class: 'wiki')
252
              end
253
              content = content_tag('div', custom_field_name_tag(value.custom_field) + ":", :class => 'label') +
254
                        content_tag('div', attr_value, :class => 'value')
255
              content = content_tag('div', content, :class => "cf_#{value.custom_field.id} attribute")
256
              s += content_tag('div', content, :class => 'splitcontent')
257
            end
258
          else
259
            lr_values = []
260
            while values.present? && ! values[0].custom_field.full_width_layout?
261
              lr_values += [ values.shift ]
262
            end
263
            half = (lr_values.size / 2.0).ceil
264
            s += issue_fields_rows do |rows|
265
              lr_values.each_with_index do |value, i|
266
                attr_value = show_value(value)
267
                if value.custom_field.text_formatting == 'full'
268
                  attr_value = content_tag('div', attr_value, class: 'wiki')
269
                end
270
                m = (i < half ? :left : :right)
271
                rows.send m, custom_field_name_tag(value.custom_field), attr_value, :class => "cf_#{value.custom_field.id}" 
272
              end
273
            end
274
          end
275
        end
258 276
      end
259

  
260
      content =
261
          content_tag('hr') +
262
          content_tag('p', content_tag('strong', custom_field_name_tag(value.custom_field) )) +
263
          content_tag('div', attr_value, class: 'value')
264
      s << content_tag('div', content, class: "cf_#{value.custom_field.id} attribute")
265 277
    end
266 278
    s
267 279
  end
app/helpers/projects_helper.rb
27 27
            {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
28 28
            {:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
29 29
            {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
30
            {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :label_time_tracking}
30
            {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :label_time_tracking},
31
            {:name => 'groupissuescustomfields', :action => :edit_project, :partial => 'projects/settings/groupissuescustomfields', :label => :grouped_cf}
31 32
            ]
32 33
    tabs.
33 34
      select {|tab| User.current.allowed_to?(tab[:action], @project)}.
app/models/attribute_group.rb
1
class AttributeGroup < ActiveRecord::Base
2
  belongs_to :project
3
  belongs_to :tracker
4
  has_many :attribute_group_fields
5
  has_many :custom_fields, :through => :attribute_group_fields
6
  acts_as_positioned
7

  
8
  scope :sorted, lambda { order(:position) }
9
end
app/models/attribute_group_field.rb
1
class AttributeGroupField < ActiveRecord::Base
2
  belongs_to :attribute_group
3
  belongs_to :custom_field
4
  has_one :tracker, :through => :attribute_group
5
  acts_as_positioned
6

  
7
  scope :sorted, lambda { order(:position) }
8
end
app/models/project.rb
51 51
  has_many :changesets, :through => :repository
52 52
  has_one :wiki, :dependent => :destroy
53 53
  # Custom field for the project issues
54
  has_many :attribute_groups
55
  has_many :attribute_group_fields, :through => :attribute_groups
54 56
  has_and_belongs_to_many :issue_custom_fields,
55 57
                          lambda {order(:position)},
56 58
                          :class_name => 'IssueCustomField',
app/models/tracker.rb
30 30
  has_many :workflow_rules, :dependent => :delete_all
31 31
  has_and_belongs_to_many :projects
32 32
  has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
33
  has_many :attribute_groups
34
  has_many :attribute_group_fields, :through => :attribute_groups
33 35
  acts_as_positioned
34 36

  
35 37
  validates_presence_of :default_status
app/views/issues/_form_custom_fields.html.erb
1
<% custom_field_values = @issue.editable_custom_field_values %>
2
<% custom_field_values_full_width = custom_field_values.select { |value| value.custom_field.full_width_layout? } %>
3
<% custom_field_values -= custom_field_values_full_width %>
4

  
5
<% if custom_field_values.present? %>
1
<% group_by_keys(@issue.project_id, @issue.tracker_id, @issue.editable_custom_field_values).each do |title,values| %>
2
<% if values.present? %>
3
<%= content_tag('h4', title, :style => 'background: #0001; padding: 0.3em;') unless title.nil? %>
4
<% while values.present? %>
6 5
<div class="splitcontent">
6
<% if values[0].custom_field.full_width_layout? %>
7
<% while values.present? && values[0].custom_field.full_width_layout? %>
8
<% value = values.shift %>
9
 <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
10
<% end %>
11
<% else %>
12
<% lr_values = [] %>
13
<% while values.present? && ! values[0].custom_field.full_width_layout? %>
14
<% lr_values += [ values.shift ] %>
15
<% end %>
7 16
<div class="splitcontentleft">
8 17
<% i = 0 %>
9
<% split_on = (custom_field_values.size / 2.0).ceil - 1 %>
10
<% custom_field_values.each do |value| %>
11
  <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
12
<% if i == split_on -%>
18
<% split_on = (lr_values.size / 2.0).ceil - 1 %>
19
<% lr_values.each do |value| %>
20
 <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
21
<% if i == split_on %>
13 22
</div><div class="splitcontentright">
14
<% end -%>
15
<% i += 1 -%>
16
<% end -%>
23
<% end %>
24
<% i += 1 %>
25
<% end %>
17 26
</div>
27
<% end %>
18 28
</div>
19 29
<% end %>
20

  
21
<% custom_field_values_full_width.each do |value| %>
22
  <p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p>
30
<% end %>
23 31
<% end %>
app/views/issues/show.html.erb
71 71
    rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
72 72
  end
73 73
end %>
74
<%= render_half_width_custom_fields_rows(@issue) %>
74
<%= render_custom_fields_rows(@issue) %>
75 75
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
76 76
</div>
77 77

  
......
94 94
  <%= link_to_attachments @issue, :thumbnails => true %>
95 95
<% end %>
96 96

  
97
<%= render_full_width_custom_fields_rows(@issue) %>
98

  
99 97
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
100 98

  
101 99
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
app/views/projects/settings/_groupissuescustomfields.html.erb
1
<p><%= select_tag "tracker_id", options_from_collection_for_select(@project.trackers.collect, "id", "name"), {:required => true, :onchange => "refresh_trackers(this);"} %><p>
2

  
3
<div id='custom_fields_form'>
4
  <% @project.trackers.each do |t| %>
5
  <div id='tracker_<%= t.id %>' class='tracker'>
6
    <span style="color: #888;"><%= l(:changed_cf_position) %></span>
7
    <ul class='sortable_items sortable_tracker-<%= t.id %> nil not_a_group'>
8
      <% @cfs.select{|x| x[0]==t.id && x[2]==nil}.each do |x| %>
9
      <li id="<%= x[6] %>" pos="<%= x[8] %>"><%= x[7] %></li>
10
      <% end %>
11
    </ul>
12
    <span style="color: #888;"><%= l(:global_cf_position) %></span>
13
    <ul class="sortable_items sortable_tracker-<%= t.id %> unsorted not_a_group">
14
      <% Issue.new(project_id: @project.id, tracker_id: t.id).custom_field_values.
15
           select{|x| ! @cfs.select{|c| c[0]==t.id}.map{|c| c[6]}.include?(x.custom_field_id)}.
16
           collect{|x| [x.custom_field.id, x.custom_field.name, x.custom_field.position]}.each do |x| %>
17
      <li id="<%= x[0] %>" pos="<%= x[2] %>"><%= x[1] %></li>
18
      <% end %>
19
    </ul>
20
    <span style="color: #888;"><%= l(:grouped_cf) %></span>
21
    <div class='sortable_groups groups_tracker-<%= t.id %>'>
22
      <% @cfs.select{|x| x[0]==t.id && x[2]!=nil}.map{|x| x[2]}.uniq.each do |g| %>
23
      <div class="sortable_group">
24
        <input type="text" value="<%= g %>" style="background: #ddf; padding: 5px; width: 360px"/>
25
        <img src="/images/delete.png" onclick="remove_label(this, 'tracker-<%= t.id %>');"/>
26
        <ul class='sortable_items sortable_tracker-<%= t.id %>'>
27
          <% @cfs.select{|x| x[0]==t.id && x[2]==g}.each do |x| %>
28
          <li id="<%= x[6] %>" pos="<%= x[8] %>"><%= x[7] %></li>
29
          <% end %>
30
        </ul>
31
      </div>
32
      <% end %>
33
      <div>
34
        <input type="text" value="" style="background: #ddf; padding: 5px; width: 370px"/>
35
        <img src="/images/add.png" style="" onclick="add_label(this, 'tracker-<%= t.id %>');"/>
36
      </div>
37
    </div>
38
  </div>
39
  <% end %>
40
<%= form_for @project, :url => { :action => 'groupissuescustomfields', :id => @project },
41
            :html => {:id => 'groupissuescustomfields-form',
42
                      :method => :post} do |f| %>
43

  
44
<%= hidden_field :group_issues_custom_fields, '', :id => 'group_issues_custom_fields', :name => 'group_issues_custom_fields' %>
45
<p><%= submit_tag l(:button_save), :onclick => "fill_json_data();" %></p>
46
<% end %>
47
</div>
48

  
49
<script>
50
function init_sortables(labelclass) {
51
  $( '.groups_' + labelclass ).sortable({
52
    items: 'div.sortable_group',
53
    connectWith: '.groups_' + labelclass,
54
    start: function(event, ui) {
55
      ui.placeholder.height(ui.item.height());
56
    },
57
    axis: 'y'
58
  });
59
  $( '.sortable_' + labelclass ).sortable({
60
    connectWith: '.sortable_' + labelclass,
61
    update: function(event, ui) {
62
      if (ui.item.parent().hasClass('unsorted')) {
63
        ui.item.parent().prepend(ui.item);
64
        ui.item.parent().children('li').each(function () {
65
          if (parseInt($(this).attr('pos')) < parseInt(ui.item.attr('pos'))) {
66
            ui.item.insertAfter(this);
67
          }
68
        });
69
      }
70
    },
71
    axis: 'y'
72
  }).disableSelection();
73
}
74
function refresh_trackers(tracker) {
75
  $('.tracker').hide();
76
  $('#tracker_' + tracker.value).show();
77
}
78
function add_label(label, labelclass) {
79
  var old_label = $(label), new_label = $(label).parent().clone();
80
  new_label.children().first().val('').trigger('change');
81
  old_label.attr('src','/images/delete.png').attr('onclick','remove_label(this, "' + labelclass + '");');
82
  old_label.prev().width('360px');
83
  old_label.parent().addClass('ui-sortable-handle sortable_group');
84
  old_label.parent().sortable({
85
    connectWith: '.groups_' + labelclass
86
  }).disableSelection();
87
  old_label.parent().append('<ul id="' + labelclass + '_" class="sortable_items sortable_' + labelclass + '"/>');
88
  old_label.next().sortable({
89
    connectWith: '.sortable_' + labelclass
90
  }).disableSelection();
91
  old_label.parent().parent().append(new_label);
92
}
93
function remove_label(label, labelclass) {
94
  $(label).next().children().each(function () {
95
    var item = this;
96
    $('.unsorted.sortable_' + labelclass).prepend(this);
97
    $('.unsorted.sortable_' + labelclass).children('li').each(function () {
98
      if (parseInt($(this).attr('pos')) < parseInt($(item).attr('pos'))) {
99
        $(item).insertAfter(this);
100
      }
101
    });
102
  });
103
  $(label).parent().hide();
104
}
105
function fill_json_data() {
106
  var r = {}, gp = 0, cp = 0;
107
  $('#custom_fields_form').children('div').each(function () {
108
    var tracker_id = this.id.split('_').pop();
109
    gp = 0, cp = 0;
110

  
111
    // Group 'nil'
112
    r[tracker_id] = {};
113
    r[tracker_id][++gp] = {'cfs': {}};
114
    $(this).children('.nil').children().each(function () {
115
      cp++;
116
      r[tracker_id][gp]['cfs'][cp] = this.id;
117
    });
118
    if (! r[tracker_id][gp]['cfs'][1]) { delete r[tracker_id][gp]; }
119
    $(this).children('.sortable_groups').children().each(function () {
120
      if ($(this).children('ul').length) {
121
        r[tracker_id][++gp] = {
122
          'name': $(this).children('input').val(),
123
          'cfs': {}
124
        };
125
        cp = 0;
126
        $(this).children('ul').children().each(function () {
127
          r[tracker_id][gp]['cfs'][++cp] = this.id;
128
        });
129
        if (! r[tracker_id][gp]['cfs'][1]) { r[tracker_id][gp] = ''; }
130
      }
131
    });
132
    if ( ! r[tracker_id] ) { r[tracker_id] = ''; }
133
  });
134
  console.log(r);
135
  $('#group_issues_custom_fields').val(JSON.stringify(r)); }
136
$('#tracker_id').change();
137
<% @project.trackers.each do |t| %>
138
init_sortables('tracker-<%= t.id %>');
139
<% end %>
140
</script>
config/locales/en.yml
1223 1223
  description_issue_category_reassign: Choose issue category
1224 1224
  description_wiki_subpages_reassign: Choose new parent page
1225 1225
  text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1226
  grouped_cf: Grouped Custom Fields
1227
  global_cf_position: Global Custom Fields Position
1228
  changed_cf_position: Changed Custom Fields Position
1226 1229
  text_login_required_html: When not requiring authentication, public projects and their contents are openly available on the network. You can <a href="%{anonymous_role_path}">edit the applicable permissions</a>.
1227 1230
  label_login_required_yes: "Yes"
1228 1231
  label_login_required_no: "No, allow anonymous access to public projects"
config/locales/pt-BR.yml
1239 1239
  permission_view_news: Ver notícias
1240 1240
  label_no_preview_alternative_html: Visualização não disponível. Faça o %{link} do arquivo.
1241 1241
  label_no_preview_download: download
1242
  grouped_cf: Agrupar campos personalizados
1243
  global_cf_position: Posição global de campos personalizados
1244
  changed_cf_position: Alterar posição de campos personalizados
1242 1245
  setting_close_duplicate_issues: Fechar tarefas duplicadas automaticamente
1243 1246
  error_exceeds_maximum_hours_per_day: Não é possível registrar mais de %{max_hours} horas no mesmo dia (%{logged_hours} horas já foram registradas)
1244 1247
  setting_time_entry_list_defaults: Registro de horas padrão
config/routes.rb
106 106
    member do
107 107
      get 'settings(/:tab)', :action => 'settings', :as => 'settings'
108 108
      post 'archive'
109
      post 'groupissuescustomfields'
109 110
      post 'unarchive'
110 111
      post 'close'
111 112
      post 'reopen'
db/migrate/20180913211420_create_attribute_groups.rb
1
class CreateAttributeGroups < ActiveRecord::Migration
2
  def change
3
    create_table :attribute_groups do |t|
4
      t.references :project, index: true, foreign_key: true
5
      t.references :tracker, index: true, foreign_key: true
6
      t.string :name
7
      t.integer :position, :default => 1, :null => false
8

  
9
      t.timestamps null: false
10
    end
11
  end
12
end
db/migrate/20180913212008_create_attribute_group_fields.rb
1
class CreateAttributeGroupFields < ActiveRecord::Migration
2
  def change
3
    create_table :attribute_group_fields do |t|
4
      t.references :attribute_group, index: true, foreign_key: true
5
      t.references :custom_field, index: true, foreign_key: true
6
      t.integer :position
7

  
8
      t.timestamps null: false
9
    end
10
  end
11
end
lib/redmine.rb
77 77
  map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
78 78
  map.permission :search_project, {:search => :index}, :public => true, :read => true
79 79
  map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
80
  map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
80
  map.permission :edit_project, {:projects => [:settings, :edit, :update, :groupissuescustomfields]}, :require => :member
81 81
  map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
82 82
  map.permission :select_project_modules, {:projects => :modules}, :require => :member
83 83
  map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true
public/stylesheets/application.css
475 475
div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
476 476
div.issue .next-prev-links {color:#999;}
477 477
div.issue .attributes {margin-top: 2em;}
478
div.issue .attributes .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
479
div.issue .attributes .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;  overflow:hidden; text-overflow: ellipsis;}
478
div.issue .attributes .attribute {padding-left:225px; clear:left; min-height: 1.8em;}
479
div.issue .attributes .attribute .label {width: 215px; margin-left:-225px; font-weight:bold; float:left; overflow:hidden; text-overflow: ellipsis;}
480 480
div.issue .attribute .value {overflow:auto; text-overflow: ellipsis;}
481
div.issue .attribute .value p {margin: 0 0 0.8em 0;}
481 482
div.issue.overdue .due-date .value { color: #c22; }
482 483

  
483 484
#issue_tree table.issues, #relations table.issues { border: 0; }
......
1549 1550
  max-height: 100%;
1550 1551
  max-width: 100%;
1551 1552
}
1553

  
1554
/* Custom Field Groups */
1555
.sortable_groups {
1556
  background: #eee;
1557
  border: 1px solid #888;
1558
  width: 445px;
1559
  min-height: 30px;
1560
  margin: 5px;
1561
  padding: 5px;
1562
}
1563
.sortable_groups div {
1564
  background: #ddf;
1565
  border: 1px solid #888;
1566
  min-height: 30px;
1567
  margin: 5px;
1568
  padding: 5px;
1569
}
1570
.sortable_items {
1571
  background: #eee;
1572
  border: 1px solid #888;
1573
  width: 400px;
1574
  min-height: 30px;
1575
  margin: 5px;
1576
  padding: 5px;
1577
}
1578
.sortable_items li {
1579
  background: #ffffff;
1580
  border: 1px solid #888;
1581
  margin: 5px;
1582
  padding: 5px;
1583
  list-style-type: none;
1584
  font-size: 1.2em;
1585
}
1586
.not_a_group {
1587
  padding: 5px 27px;
1588
}
1589
.ui-sortable-handle:before {
1590
  content:url('/images/reorder.png');
1591
  margin-right: 5px;
1592
}
1593
.ui-sortable-placeholder {
1594
  background: #8888ff;
1595
  border: 1px solid #888;
1596
  visibility: visible;
1597
}
test/fixtures/attribute_group_fields.yml
1
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2

  
3
one:
4
  attribute_group_id: 
5
  custom_field_id: 
6
  position: 1
7

  
8
two:
9
  attribute_group_id: 
10
  custom_field_id: 
11
  position: 1
test/fixtures/attribute_groups.yml
1
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2

  
3
one:
4
  project_id: 
5
  tracker_id: 
6
  name: MyString
7
  position: 1
8

  
9
two:
10
  project_id: 
11
  tracker_id: 
12
  name: MyString
13
  position: 1
(7-7/24)