Feature #13839 » custom_field_layout_editing.diff
app/helpers/projects_helper.rb | ||
---|---|---|
22 | 22 |
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, |
23 | 23 |
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, |
24 | 24 |
{:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural}, |
25 |
{:name => 'custom_fields_layouts', :action => :manage_custom_fields_layout, :partial => 'projects/settings/custom_fields_layout', :label => :label_custom_field_plural}, |
|
25 | 26 |
{:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, |
26 | 27 |
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}, |
27 | 28 |
{:name => 'resolutions', :action => :manage_resolutions, :partial => 'projects/settings/issue_resolutions', :label => :label_issue_resolution_plural}, |
app/helpers/custom_fields_helper.rb | ||
---|---|---|
85 | 85 | |
86 | 86 |
content_tag "label", content + |
87 | 87 |
(required ? " <span class=\"required\">*</span>".html_safe : ""), |
88 |
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}" |
|
88 |
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}", :class => "custom_field_label"
|
|
89 | 89 |
end |
90 | 90 | |
91 | 91 |
# Return custom field tag with its label tag |
app/models/custom_field_layout.rb | ||
---|---|---|
1 |
class CustomFieldLayout < ActiveRecord::Base |
|
2 |
|
|
3 |
end |
app/controllers/custom_fields_layouts_controller.rb | ||
---|---|---|
1 |
class CustomFieldsLayoutsController < ApplicationController |
|
2 |
helper :all |
|
3 |
before_filter :find_custom_field_values |
|
4 | ||
5 |
def new |
|
6 |
@layout = CustomFieldLayout.new(project_id: @project.id, tracker_id: @tracker.id) |
|
7 |
end |
|
8 |
|
|
9 |
def apply |
|
10 |
if @cf_layout.nil? |
|
11 |
@layout = CustomFieldLayout.new(project_id: @project.id, tracker_id: @tracker.id) |
|
12 |
@layout.location = JSON.parse(params[:ids]) |
|
13 |
@layout.project_id = @project.id |
|
14 |
@layout.tracker_id = @tracker.id |
|
15 |
if @layout.save |
|
16 |
redirect_to settings_project_path(@project) |
|
17 |
end |
|
18 |
else |
|
19 |
@cf_layout.location = JSON.parse(params[:ids]) |
|
20 |
if @cf_layout.save |
|
21 |
redirect_to settings_project_path(@project) |
|
22 |
end |
|
23 |
end |
|
24 |
end |
|
25 | ||
26 |
def show |
|
27 |
respond_to do |format| |
|
28 |
format.js |
|
29 |
end |
|
30 |
end |
|
31 |
|
|
32 |
private |
|
33 |
def find_custom_field_values |
|
34 |
@project = Project.find(params[:project_id]) |
|
35 |
@tracker = Tracker.find(params[:layout][:tracker_id]) |
|
36 |
|
|
37 |
#get last edited custom field location |
|
38 |
@cf_layout = CustomFieldLayout.find_by({:tracker_id => @tracker.id, :project_id => @project.id}) |
|
39 |
cf_ids = @cf_layout.nil? ? [[],[]] : JSON.parse(@cf_layout.location.gsub("nil","null")) |
|
40 |
|
|
41 |
#get ids of nil elements |
|
42 |
ids_of_nil = [[],[]] |
|
43 |
cf_ids.each_index{ |i| cf_ids[i].each_index.select{|k| ids_of_nil[i] << k if cf_ids[i][k].nil?}}.map!{|i| i -= [nil]} |
|
44 |
|
|
45 |
#get default custom field location |
|
46 |
default_values = (@project.issues.new(:tracker_id => @tracker.id, |
|
47 |
:subject => " ", :author_id => @current_user).editable_custom_field_values) |
|
48 | ||
49 |
#if not set yet then default |
|
50 |
if !cf_ids.all?{|array| array.empty?} |
|
51 |
ordered_values = default_values.group_by(&:custom_field_id).values_at(*cf_ids.flatten).flatten(1) |
|
52 |
added = default_values - ordered_values |
|
53 |
@custom_fields_values = [ordered_values[0 ... cf_ids.first.count], |
|
54 |
ordered_values[cf_ids.first.count .. -1] + added] |
|
55 |
ids_of_nil.each_index{|i| ids_of_nil[i].each{|v| @custom_fields_values[i].insert(v,nil)}} |
|
56 |
else |
|
57 |
@custom_fields_values = [default_values[0 ... default_values.count/2], default_values[default_values.count/2 .. -1]] |
|
58 |
end |
|
59 |
end |
|
60 |
end |
app/controllers/projects_controller.rb | ||
---|---|---|
161 | 161 |
@member ||= @project.members.new |
162 | 162 |
@trackers = Tracker.sorted.to_a |
163 | 163 |
@wiki ||= @project.wiki || Wiki.new(:project => @project) |
164 |
@layout = CustomFieldLayout.new() |
|
165 |
|
|
164 | 166 |
end |
165 | 167 | |
166 | 168 |
def edit |
app/views/custom_fields_layouts/show.js.erb | ||
---|---|---|
1 |
$("#cfv").html('<%= escape_javascript(render(partial: "show"))%>'); |
|
2 |
app/views/custom_fields_layouts/_custom_fields_layout.html.erb | ||
---|---|---|
1 |
<div class="splitcontent"> |
|
2 |
<div class="splitcontentleft"> |
|
3 |
<div class="sortable"> |
|
4 |
<% i = 0 %> |
|
5 |
<% custom_fields_values.each do |side|%> |
|
6 |
<% side.each do |value|%> |
|
7 |
<% if !value.nil? %> |
|
8 |
<p class="custom_field_p" id=<%=value.custom_field_id%>> |
|
9 |
<%= custom_field_tag_with_label :issue, value, :required => issue.required_attribute?(value.custom_field_id) %> |
|
10 |
</p> |
|
11 |
<%else %> |
|
12 |
<p class="custom_field_p clean"%></p> |
|
13 |
<% end %> |
|
14 |
<% end -%> |
|
15 |
<% i = i + 1 %> |
|
16 |
<% if (i == 1) %> |
|
17 |
</div></div> <div class="splitcontentright"> <div class="sortable"> |
|
18 |
<% end -%> |
|
19 |
<% end -%> |
|
20 |
</div> |
|
21 |
<p> </p> |
|
22 |
</div> |
|
23 |
</div> |
|
24 |
app/views/custom_fields_layouts/_show.html.erb | ||
---|---|---|
1 |
<% issue = @project.issues.new(tracker_id: @tracker.id, subject: " ", author_id: @current_user) %> |
|
2 | ||
3 |
<%= render partial: "custom_fields_layout" , locals: {issue: issue, custom_fields_values: @custom_fields_values} %> |
|
4 | ||
5 |
app/views/projects/settings/_custom_fields_layout.html.erb | ||
---|---|---|
1 |
<h2><%= l(:label_custom_field_layout) %></h2> |
|
2 |
<script> |
|
3 | ||
4 |
function addBlank(){ |
|
5 |
$(".splitcontent").children().find(".sortable").each( |
|
6 |
function(){ |
|
7 |
var cf_count = $(this).children().length; |
|
8 |
if (cf_count == 0){ $(this).append("<p class='custom_field_p clean'> </p>") }; |
|
9 |
}); |
|
10 | ||
11 |
} |
|
12 | ||
13 |
/*add sortable ui to added elements*/ |
|
14 |
function loadCf(){ |
|
15 |
if($('.custom_field_label').length != 0) { |
|
16 |
|
|
17 |
/*add fields for removing/adding if there is smth to add/remove */ |
|
18 |
$(".add_remove").append(" <p class='custom_field_remove' > <%= l(:label_remove_field) %> </p>"); |
|
19 |
$(".copy").append("<p class='custom_field_p clean'> <%= l(:label_move_field) %> </p> "); |
|
20 | ||
21 |
/*add blank div by default if any splitcontent is fully empty |
|
22 |
to show where we can move element*/ |
|
23 |
addBlank(); |
|
24 |
|
|
25 |
/*make borders of divs visible*/ |
|
26 |
$(".custom_field_p, .custom_field_remove").css({"border" : "thin solid", |
|
27 |
"border-color" : "#ddd"}); |
|
28 |
$(".custom_field_remove").css({"border-color" : "#B00"}); |
|
29 |
$(".custom_field_p.clean").css({"height" : "2.3em"}); |
|
30 |
$(".sortable").css({"min-height": "4.6em", |
|
31 |
"border-style" : "ridge", |
|
32 |
"padding" : "4px", |
|
33 |
"border-width": "1px", |
|
34 |
"border-color" : "#dddFFF"}); |
|
35 | ||
36 | ||
37 |
/*make elements sortable (allow reordering)*/ |
|
38 |
/*not allow moving last elements in the splitcontent(left|right) */ |
|
39 |
var is_alone = 1; |
|
40 |
$(".sortable").sortable({connectWith: ".sortable", |
|
41 |
dropOnEmpty: true, |
|
42 |
receive: function(e,ui) { |
|
43 |
copyHelper= null;} |
|
44 |
}); |
|
45 | ||
46 |
/*allow to copy blank div and make it sortable*/ |
|
47 |
$( ".copy" ).sortable({ |
|
48 |
connectWith: ".sortable", |
|
49 |
forcePlaceholderSize: false, |
|
50 |
helper: function(e,li) { |
|
51 |
copyHelper= li.clone().insertAfter(li.text("")); |
|
52 |
return li.clone(); |
|
53 |
}, |
|
54 |
stop: function() { |
|
55 |
copyHelper && copyHelper.remove(); |
|
56 |
} |
|
57 |
}); |
|
58 | ||
59 |
/* make field droppable (moving div inside droppable one removes it)*/ |
|
60 |
$('.custom_field_remove').droppable({ |
|
61 |
accept: ".clean", |
|
62 |
drop: function(event, ui) { ui.draggable.remove(); } |
|
63 |
}); |
|
64 |
|
|
65 |
}}; |
|
66 | ||
67 |
/*get custom fields for selected tracker*/ |
|
68 |
function getCustomFields() { |
|
69 |
$(".clean").remove(); |
|
70 |
$(".custom_field_remove").remove(); |
|
71 |
var tracker = $("#layout_tracker_id").find(":selected").val(); |
|
72 |
$.ajax({ |
|
73 |
method: "get", |
|
74 |
url: "<%= url_for :controller=>'custom_fields_layouts', :action => 'show', :project_id => @project %>", |
|
75 |
contentType: 'application/json', |
|
76 |
data: {layout: { tracker_id: tracker}}, |
|
77 |
cache: false, |
|
78 |
error: function(jqXHR, textStatus, errorThrown){ |
|
79 |
alert('Exception' + errorThrown); |
|
80 |
}, |
|
81 |
success: function(){ loadCf(); } |
|
82 |
}) |
|
83 |
}; |
|
84 | ||
85 |
/*get custom fields after loading*/ |
|
86 |
$(document).ready(function(){ |
|
87 |
getCustomFields(); |
|
88 |
}); |
|
89 | ||
90 | ||
91 | ||
92 |
/*set custom field ids on hidden field */ |
|
93 |
function setCfIds(){ |
|
94 |
var array = []; |
|
95 |
var left = []; |
|
96 |
var right = []; |
|
97 |
$("#cfv").find(".splitcontentleft").find(".custom_field_p").each(function() { |
|
98 |
left.push(parseInt($(this).attr("id"))); |
|
99 |
}); |
|
100 |
$("#cfv").find(".splitcontentright").find(".custom_field_p").each(function() { |
|
101 |
right.push(parseInt($(this).attr("id"))); |
|
102 |
}); |
|
103 |
array.push(left,right); |
|
104 |
$("#custom_fields_locations").val(JSON.stringify(array)); |
|
105 |
} ; |
|
106 | ||
107 |
/* don't need datepicker functionality for editing locations of custom fields*/ |
|
108 | ||
109 | ||
110 | ||
111 |
</script> |
|
112 | ||
113 |
<div class="box tabular"> |
|
114 | ||
115 |
<%= labelled_form_for @layout,:as => 'layout', url: {controller: "custom_fields_layouts", |
|
116 |
action: "apply", :project_id => @project} do |f| %> |
|
117 | ||
118 |
<% include_calendar_headers_tags %> |
|
119 |
<% if @project.issues.new.safe_attribute? 'tracker_id' %> |
|
120 |
<p><%= f.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]}, {:required => true}, |
|
121 |
:onchange => "getCustomFields();" %></p> |
|
122 |
<%end%> |
|
123 |
|
|
124 |
<div class= "add_remove"> </div><p > </p> |
|
125 |
|
|
126 |
<div id="cfv"> </div> |
|
127 | ||
128 |
<%= hidden_field_tag :ids, "", :id => "custom_fields_locations" %> |
|
129 |
<%= submit_tag l(:button_update), :onclick => "setCfIds();" %> |
|
130 |
|
|
131 |
<div class ="splitcontentright copy"> </div> |
|
132 | ||
133 |
<%end %> |
|
134 |
</div> |
app/views/issues/show.html.erb | ||
---|---|---|
72 | 72 |
rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? link_to(l_hours(@issue.total_spent_hours), issue_time_entries_path(@issue)) : "-"), :class => 'spent-time' |
73 | 73 |
end |
74 | 74 |
end %> |
75 | ||
76 | ||
75 | 77 |
<%= render_custom_fields_rows(@issue) %> |
76 | 78 |
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> |
77 | 79 |
</table> |
80 | ||
81 |
lib/redmine.rb | ||
---|---|---|
86 | 86 |
map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member |
87 | 87 |
map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member |
88 | 88 |
map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member |
89 | ||
89 |
map.permission :manage_custom_fields_layout, {:projects => :settings, :custom_fields_editing => [:apply]}, :require => :member |
|
90 |
|
|
90 | 91 |
map.project_module :issue_tracking do |map| |
91 | 92 |
# Issues |
92 | 93 |
map.permission :view_issues, {:issues => [:index, :show], |
app/views/issues/_form_custom_fields.html.erb | ||
---|---|---|
1 |
<% custom_field_values = @issue.editable_custom_field_values %> |
|
2 |
<% if custom_field_values.present? %> |
|
3 |
<div class="splitcontent"> |
|
4 |
<div class="splitcontentleft"> |
|
5 |
<% i = 0 %> |
|
6 |
<% split_on = (custom_field_values.size / 2.0).ceil - 1 %> |
|
7 |
<% custom_field_values.each do |value| %> |
|
8 |
<p><%= custom_field_tag_with_label :issue, value, :required => @issue.required_attribute?(value.custom_field_id) %></p> |
|
9 |
<% if i == split_on -%> |
|
10 |
</div><div class="splitcontentright"> |
|
11 |
<% end -%> |
|
12 |
<% i += 1 -%> |
|
13 |
<% end -%> |
|
14 |
</div> |
|
15 |
</div> |
|
16 |
<% end %> |
|
1 |
<% cf_layout = CustomFieldLayout.all.where(:tracker_id => @issue.tracker_id, :project_id => @project.id).last %> |
|
2 |
<% default_values = @issue.editable_custom_field_values %> |
|
3 |
<% cf_ids = cf_layout.nil? ? [[],[]] : JSON.parse(cf_layout.location.gsub("nil","null")) %> |
|
4 |
|
|
5 |
<% ids_of_nil = [[],[]] %> |
|
6 |
<% cf_ids.each_index{ |i| cf_ids[i].each_index.select{|k| ids_of_nil[i] << k if cf_ids[i][k].nil?}}.map!{|i| i -= [nil]} %> |
|
7 | ||
8 |
<% if !cf_ids.all?{|array| array.empty?} %> |
|
9 |
<% ordered_values = default_values.group_by(&:custom_field_id).values_at(*cf_ids.flatten).flatten(1) %> |
|
10 |
<% added = default_values - ordered_values %> |
|
11 |
<% custom_fields_values = [ordered_values[0 ... cf_ids.first.count], |
|
12 |
ordered_values[cf_ids.first.count .. -1] + added] %> |
|
13 |
<% ids_of_nil.each_index{|i| ids_of_nil[i].each{|v| custom_fields_values[i].insert(v,nil)}} %> |
|
14 |
<% else %> |
|
15 |
<% custom_fields_values = [default_values[0 ... default_values.count/2], default_values[default_values.count/2 .. -1]] %> |
|
16 |
<% end %> |
|
17 | ||
18 |
<%= render partial: "custom_fields_layouts/custom_fields_layout" , locals: {issue: @issue, custom_fields_values: custom_fields_values} %> |
|
19 | ||
20 |
config/routes.rb | ||
---|---|---|
136 | 136 |
get 'versions.:format', :to => 'versions#index' |
137 | 137 |
get 'roadmap', :to => 'versions#index', :format => false |
138 | 138 |
get 'versions', :to => 'versions#index' |
139 |
|
|
140 |
resources :custom_fields_layouts, :only => [:show] |
|
141 |
match 'settings/custom_fields_layouts', :to => 'custom_fields_layouts#apply', :via => [:post] |
|
139 | 142 | |
140 | 143 |
resources :news, :except => [:show, :edit, :update, :destroy] |
141 | 144 |
resources :time_entries, :controller => 'timelog', :except => [:show, :edit, :update, :destroy] do |
app/helpers/issues_helper.rb | ||
---|---|---|
220 | 220 |
r.to_html |
221 | 221 |
end |
222 | 222 | |
223 |
#add empty elements for proper cfs showing |
|
224 |
def add_nills(cfs) |
|
225 |
diff = cfs[0].count - cfs[1].count |
|
226 |
if (diff > 0) |
|
227 |
cfs[1] += [nil] * diff |
|
228 |
elsif (diff < 0) |
|
229 |
cfs[0] += [nil] * (-diff) |
|
230 |
end |
|
231 |
cfs |
|
232 |
end |
|
233 | ||
223 | 234 |
def render_custom_fields_rows(issue) |
224 |
values = issue.visible_custom_field_values |
|
225 |
return if values.empty? |
|
226 |
half = (values.size / 2.0).ceil |
|
227 |
issue_fields_rows do |rows| |
|
228 |
values.each_with_index do |value, i| |
|
229 |
css = "cf_#{value.custom_field.id}" |
|
230 |
m = (i < half ? :left : :right) |
|
231 |
rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css |
|
235 |
default_values = issue.visible_custom_field_values |
|
236 |
return if default_values.empty? |
|
237 | ||
238 |
#get last edited custom field location |
|
239 |
cf_layout = CustomFieldLayout.find_by(:tracker_id => issue.tracker_id, :project_id => issue.project_id) |
|
240 |
cf_ids = cf_layout.nil? ? [[],[]] : JSON.parse(cf_layout.location.gsub("nil","null")) |
|
241 | ||
242 |
#get ids of nil elements |
|
243 |
ids_of_nil = [[],[]] |
|
244 |
cf_ids.each_index{ |i| cf_ids[i].each_index.select{|k| ids_of_nil[i] << k if cf_ids[i][k].nil?}}.map!{|i| i -= [nil]} |
|
245 | ||
246 |
#if not set yet then default |
|
247 |
if !cf_ids.all?{|array| array.empty?} |
|
248 |
ordered_values = default_values.group_by(&:custom_field_id).values_at(*cf_ids.flatten).flatten(1) |
|
249 |
added = default_values - ordered_values |
|
250 |
custom_fields_values = [ordered_values[0 ... cf_ids.first.count], |
|
251 |
ordered_values[cf_ids.first.count .. -1] + added] |
|
252 |
ids_of_nil.each_index{|i| ids_of_nil[i].each{|v| custom_fields_values[i].insert(v,nil)}} |
|
253 |
else |
|
254 |
custom_fields_values = [default_values[0 ... default_values.count/2], default_values[default_values.count/2 .. -1]] |
|
255 |
end |
|
256 |
custom_fields_values = add_nills(custom_fields_values) |
|
257 |
cfs = custom_fields_values[0].zip(custom_fields_values[1]).flatten |
|
258 | ||
259 |
s = "<tr>\n" |
|
260 |
n = 0 |
|
261 |
cfs.each do |value| |
|
262 |
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0 |
|
263 |
if value.nil? |
|
264 |
css = "custom_field_p clean" |
|
265 |
s << "\t<th class=\"#{css}\"> </th><td class=\"#{css}\"> </td>\n" |
|
266 |
else |
|
267 |
css = "cf_#{value.custom_field_id}" |
|
268 |
s << "\t<th class=\"#{css}\">#{ h(value.custom_field.name) }:</th><td class=\"#{css}\">#{ h(show_value(value)) }</td>\n" |
|
232 | 269 |
end |
270 |
n +=1 |
|
233 | 271 |
end |
272 |
s << "</tr>\n" |
|
273 |
s.html_safe |
|
234 | 274 |
end |
235 | 275 |
# Returns the path for updating the issue form |
config/locales/ru.yml | ||
---|---|---|
469 | 469 |
label_current_version: Текущая версия |
470 | 470 |
label_custom_field: Настраиваемое поле |
471 | 471 |
label_custom_field_new: Новое настраиваемое поле |
472 |
label_custom_field_layout: Редактирование расположения кастомных полей |
|
472 | 473 |
label_custom_field_plural: Настраиваемые поля |
473 | 474 |
label_date_from: С |
474 | 475 |
label_date_from_to: С %{start} по %{end} |
... | ... | |
577 | 578 |
label_month: Месяц |
578 | 579 |
label_more_than_ago: более, чем дней(я) назад |
579 | 580 |
label_more: Больше |
581 |
label_move_field: Переместите пустое поле |
|
580 | 582 |
label_my_account: Моя учётная запись |
581 | 583 |
label_my_page: Моя страница |
582 | 584 |
label_my_page_block: Блок моей страницы |
... | ... | |
625 | 627 |
label_project_plural2: проекта |
626 | 628 |
label_project_plural5: проектов |
627 | 629 |
label_public_projects: Общие проекты |
630 |
label_remove_field: Удалите пустое поле, перреместив его сюда |
|
628 | 631 |
label_query: Сохранённый запрос |
629 | 632 |
label_query_new: Новый запрос |
630 | 633 |
label_query_plural: Сохранённые запросы |
config/locales/en.yml (revision 1956) | ||
---|---|---|
551 | 551 |
label_custom_field: Custom field |
552 | 552 |
label_custom_field_plural: Custom fields |
553 | 553 |
label_custom_field_new: New custom field |
554 |
label_custom_field_layout: Edit custom fields layout |
|
554 | 555 |
label_enumerations: Enumerations |
555 | 556 |
label_enumeration_new: New value |
556 | 557 |
label_information: Information |
... | ... | |
560 | 561 |
label_login_with_open_id_option: or login with OpenID |
561 | 562 |
label_password_lost: Lost password |
562 | 563 |
label_home: Home |
564 |
label_move_field: Move blank field |
|
563 | 565 |
label_my_page: My page |
564 | 566 |
label_my_account: My account |
565 | 567 |
label_my_projects: My projects |
... | ... | |
572 | 574 |
label_assigned_to_me_issues: Issues assigned to me |
573 | 575 |
label_last_login: Last connection |
574 | 576 |
label_registered_on: Registered on |
577 |
label_remove_field: Remove blank fields moving them here |
|
575 | 578 |
label_activity: Activity |
576 | 579 |
label_overall_activity: Overall activity |
577 | 580 |
label_user_activity: "%{value}'s activity" |
- « Previous
- 1
- 2
- 3
- 4
- Next »