Feature #4272 » dynamic-issue-columns.patch
app/controllers/issues_controller.rb | ||
---|---|---|
22 | 22 |
before_filter :find_issue, :only => [:show, :edit, :reply] |
23 | 23 |
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy] |
24 | 24 |
before_filter :find_project, :only => [:new, :update_form, :preview] |
25 |
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu] |
|
25 |
before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu, :update_column]
|
|
26 | 26 |
before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar] |
27 | 27 |
accept_key_auth :index, :show, :changes |
28 | 28 | |
... | ... | |
415 | 415 |
end |
416 | 416 |
|
417 | 417 |
def context_menu |
418 |
@issues = Issue.find_all_by_id(params[:ids], :include => :project) |
|
419 |
if (@issues.size == 1) |
|
420 |
@issue = @issues.first |
|
421 |
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) |
|
418 |
# Wants issue context menu |
|
419 |
if params[:header_context_menu].nil? && !params[:ids].blank? |
|
420 |
@issues = Issue.find_all_by_id(params[:ids], :include => :project) |
|
421 |
if (@issues.size == 1) |
|
422 |
@issue = @issues.first |
|
423 |
@allowed_statuses = @issue.new_statuses_allowed_to(User.current) |
|
424 |
end |
|
425 |
projects = @issues.collect(&:project).compact.uniq |
|
426 |
@project = projects.first if projects.size == 1 |
|
427 | ||
428 |
@can = { |
|
429 |
:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), |
|
430 |
:log_time => (@project && User.current.allowed_to?(:log_time, @project)), |
|
431 |
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), |
|
432 |
:move => (@project && User.current.allowed_to?(:move_issues, @project)), |
|
433 |
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), |
|
434 |
:delete => (@project && User.current.allowed_to?(:delete_issues, @project)) |
|
435 |
} |
|
436 | ||
437 |
if @project |
|
438 |
@assignables = @project.assignable_users |
|
439 |
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) |
|
440 |
end |
|
441 | ||
442 |
@priorities = IssuePriority.all.reverse |
|
443 |
@statuses = IssueStatus.find(:all, :order => 'position') |
|
444 |
@back = params[:back_url] || request.env['HTTP_REFERER'] |
|
445 |
|
|
446 |
render :layout => false |
|
447 |
else |
|
448 |
# Wants a header context menu |
|
449 |
find_project unless params[:project_id].blank? |
|
450 |
retrieve_query |
|
451 |
@back = params[:back_url] || request.env['HTTP_REFERER'] |
|
452 |
|
|
453 |
render :action => 'header_context_menu', :layout => false |
|
422 | 454 |
end |
423 |
projects = @issues.collect(&:project).compact.uniq |
|
424 |
@project = projects.first if projects.size == 1 |
|
455 |
end |
|
456 | ||
457 |
# Takes the params and stores the updated column information from the index |
|
458 |
def update_column |
|
459 |
find_project unless params[:project_id].blank? |
|
460 |
retrieve_query |
|
425 | 461 | |
426 |
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)), |
|
427 |
:log_time => (@project && User.current.allowed_to?(:log_time, @project)), |
|
428 |
:update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))), |
|
429 |
:move => (@project && User.current.allowed_to?(:move_issues, @project)), |
|
430 |
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)), |
|
431 |
:delete => (@project && User.current.allowed_to?(:delete_issues, @project)) |
|
432 |
} |
|
433 |
if @project |
|
434 |
@assignables = @project.assignable_users |
|
435 |
@assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to) |
|
462 |
respond_to do |format| |
|
463 |
format.html { redirect_to :action => 'index', :project_id => params[:project_id] } |
|
464 |
format.js {render(:update) {|page| page.redirect_to :action => 'index', :project_id => params[:project_id] }} |
|
436 | 465 |
end |
437 |
|
|
438 |
@priorities = IssuePriority.all.reverse |
|
439 |
@statuses = IssueStatus.find(:all, :order => 'position') |
|
440 |
@back = params[:back_url] || request.env['HTTP_REFERER'] |
|
441 |
|
|
442 |
render :layout => false |
|
443 | 466 |
end |
444 | 467 | |
445 | 468 |
def update_form |
... | ... | |
498 | 521 |
cond << " OR project_id = #{@project.id}" if @project |
499 | 522 |
@query = Query.find(params[:query_id], :conditions => cond) |
500 | 523 |
@query.project = @project |
501 |
session[:query] = {:id => @query.id, :project_id => @query.project_id} |
|
502 | 524 |
sort_clear |
525 |
@query.column_names = session[:query][:column_names] unless session[:query].nil? || session[:query][:column_names].nil? || session[:query][:id] != @query.id |
|
526 |
|
|
527 |
# Update column list |
|
528 |
if params[:column].present? |
|
529 |
@query.column_names = update_columns_from_session(@query, params[:column].to_sym) |
|
530 |
end |
|
531 |
session[:query] = {:id => @query.id, :project_id => @query.project_id, :column_names => @query.column_names} |
|
503 | 532 |
else |
504 | 533 |
if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) |
505 | 534 |
# Give it a name, required to be valid |
506 | 535 |
@query = Query.new(:name => "_") |
507 | 536 |
@query.project = @project |
537 |
@query.column_names = session[:query][:column_names] unless session[:query].nil? || session[:query][:column_names].nil? || session[:query][:id] != @query.id |
|
508 | 538 |
if params[:fields] and params[:fields].is_a? Array |
509 | 539 |
params[:fields].each do |field| |
510 | 540 |
@query.add_filter(field, params[:operators][field], params[:values][field]) |
... | ... | |
514 | 544 |
@query.add_short_filter(field, params[field]) if params[field] |
515 | 545 |
end |
516 | 546 |
end |
547 | ||
548 |
# Update column list |
|
549 |
if params[:column].present? |
|
550 |
columns = update_columns_from_session(@query, params[:column].to_sym) |
|
551 | ||
552 |
# Order columns based on the Query ordering |
|
553 |
columns = @query.available_columns.select {|c| columns.include?(c.name)}.collect(&:name) |
|
554 | ||
555 |
@query.column_names = columns |
|
556 |
end |
|
557 |
|
|
517 | 558 |
@query.group_by = params[:group_by] |
518 |
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by} |
|
559 |
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
|
|
519 | 560 |
else |
520 | 561 |
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id] |
521 |
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by]) |
|
562 |
@query.column_names = session[:query][:column_names] if session[:query][:id] && session[:query][:column_names] |
|
563 |
@query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) |
|
522 | 564 |
@query.project = @project |
523 | 565 |
end |
524 | 566 |
end |
525 | 567 |
end |
568 |
|
|
569 |
def update_columns_from_session(query, column_name) |
|
570 |
# Force to symbol |
|
571 |
column_symbol = column_name.to_sym |
|
572 |
|
|
573 |
columns = @query.columns.collect(&:name) # Current columns |
|
574 |
# Columns from session |
|
575 |
if session[:query] && session[:query][:column_names] |
|
576 |
# Set Union |
|
577 |
columns = columns | session[:query][:column_names] |
|
578 |
end |
|
579 |
|
|
580 |
# Delete if the update column exists, otherwise add it |
|
581 |
if columns.include?(column_symbol) |
|
582 |
columns.delete(column_symbol) |
|
583 |
else |
|
584 |
columns << column_symbol |
|
585 |
end |
|
586 |
|
|
587 |
return columns |
|
588 |
end |
|
526 | 589 |
end |
app/controllers/queries_controller.rb | ||
---|---|---|
27 | 27 |
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? |
28 | 28 |
@query.column_names = nil if params[:default_columns] |
29 | 29 |
|
30 |
# Use the columns from the session if they are set |
|
31 |
if @query.column_names.nil? && session[:query] && session[:query][:column_names] |
|
32 |
@query.column_names = session[:query][:column_names] |
|
33 |
end |
|
34 | ||
30 | 35 |
params[:fields].each do |field| |
31 | 36 |
@query.add_filter(field, params[:operators][field], params[:values][field]) |
32 | 37 |
end if params[:fields] |
... | ... | |
52 | 57 |
@query.column_names = nil if params[:default_columns] |
53 | 58 |
|
54 | 59 |
if @query.save |
60 |
# Clear the session[:query] to use the saved query |
|
61 |
session[:query] = { :id => @query.id, :project_id => @query.project_id, :column_names => @query.column_names} |
|
55 | 62 |
flash[:notice] = l(:notice_successful_update) |
56 | 63 |
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query |
57 | 64 |
end |
app/views/issues/_list.rhtml | ||
---|---|---|
1 | 1 |
<% form_tag({}) do -%> |
2 | 2 |
<%= hidden_field_tag 'back_url', url_for(params) %> |
3 | 3 |
<table class="list issues"> |
4 |
<thead><tr> |
|
5 |
<th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;', |
|
4 |
<thead><tr id="issue-header" class="no-select hascontextmenu"> |
|
5 |
<th><%= check_box_tag("header_context_menu", false, false, :style => 'display: none;', :class => 'no-select' ) %> |
|
6 |
<%= hidden_field_tag("query_id", @query.id) if @query %> |
|
7 |
<%= hidden_field_tag("project_id", @project.id) if @project %> |
|
8 |
<%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;', |
|
6 | 9 |
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> |
7 | 10 |
</th> |
8 | 11 |
<%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> |
app/views/issues/header_context_menu.rhtml | ||
---|---|---|
1 |
<ul> |
|
2 |
<% @query.available_columns.each do |column| %> |
|
3 |
<li> |
|
4 |
<%= link_to_remote(column.caption, |
|
5 |
{ |
|
6 |
:url => { |
|
7 |
:controller => 'issues', |
|
8 |
:action => 'update_column', |
|
9 |
:project_id => @project, |
|
10 |
:set_filter => true, |
|
11 |
:column => column.name, |
|
12 |
:query_id => @query.id |
|
13 |
}, |
|
14 |
:with => "Form.serialize('query_form')" |
|
15 |
}, |
|
16 |
{ |
|
17 |
:class => @query.columns.include?(column) ? 'icon-checked' : 'icon-none', |
|
18 |
:method => :post |
|
19 |
})%> |
|
20 |
</li> |
|
21 |
<% end %> |
|
22 |
</ul> |
public/javascripts/context_menu.js | ||
---|---|---|
34 | 34 |
var tr = Event.findElement(e, 'tr'); |
35 | 35 |
if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; } |
36 | 36 |
Event.stop(e); |
37 |
if (!this.isSelected(tr)) {
|
|
37 |
if (!this.isSelected(tr)) {
|
|
38 | 38 |
this.unselectAll(); |
39 | 39 |
this.addSelection(tr); |
40 | 40 |
this.lastSelected = tr; |
... | ... | |
48 | 48 |
if (window.opera && e.altKey) { return; } |
49 | 49 |
if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) { |
50 | 50 |
var tr = Event.findElement(e, 'tr'); |
51 |
if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu')) {
|
|
51 |
if (tr!=null && tr!=document && tr.hasClassName('hascontextmenu') && !tr.hasClassName('no-select')) {
|
|
52 | 52 |
// a row was clicked, check if the click was on checkbox |
53 | 53 |
var box = Event.findElement(e, 'input'); |
54 | 54 |
if (box!=document && box!=undefined) { |
... | ... | |
155 | 155 |
}, |
156 | 156 |
|
157 | 157 |
addSelection: function(tr) { |
158 |
tr.addClassName('context-menu-selection'); |
|
158 |
if (!tr.hasClassName('no-select')) { |
|
159 |
tr.addClassName('context-menu-selection'); |
|
160 |
} |
|
159 | 161 |
this.checkSelectionBox(tr, true); |
160 | 162 |
}, |
161 | 163 |
|
... | ... | |
197 | 199 |
if (all_checked) { |
198 | 200 |
boxes[i].checked = false; |
199 | 201 |
boxes[i].up('tr').removeClassName('context-menu-selection'); |
200 |
} else if (boxes[i].checked == false) { |
|
202 |
} else if (boxes[i].checked == false && !boxes[i].hasClassName('no-select')) {
|
|
201 | 203 |
boxes[i].checked = true; |
202 | 204 |
boxes[i].up('tr').addClassName('context-menu-selection'); |
203 | 205 |
} |
test/functional/issues_controller_test.rb | ||
---|---|---|
1093 | 1093 |
:attributes => { :href => '#', |
1094 | 1094 |
:class => 'icon-del disabled' } |
1095 | 1095 |
end |
1096 |
|
|
1096 | ||
1097 |
def test_context_menu_headers |
|
1098 |
@request.session[:user_id] = 2 |
|
1099 |
get :context_menu, :header_context_menu => '1' |
|
1100 |
assert_response :success |
|
1101 |
assert_template 'header_context_menu' |
|
1102 | ||
1103 |
assert_select 'ul' do |
|
1104 |
assert_select 'a', 'Project' |
|
1105 |
assert_select 'a', 'Tracker' |
|
1106 |
end |
|
1107 |
end |
|
1108 | ||
1109 |
def test_update_column_to_add_a_column |
|
1110 |
@request.session[:user_id] = 2 |
|
1111 |
post :update_column, :project_id => 'ecookbook', :column => 'author' |
|
1112 |
assert_redirected_to :action =>'index', :project_id => 'ecookbook' |
|
1113 | ||
1114 |
assert session[:query] |
|
1115 |
assert session[:query][:column_names] |
|
1116 |
assert session[:query][:column_names].include?(:author) |
|
1117 |
end |
|
1118 | ||
1119 |
def test_update_column_to_remove_a_column |
|
1120 |
@request.session[:user_id] = 2 |
|
1121 |
post :update_column, :project_id => 'ecookbook', :column => 'status' |
|
1122 |
assert_redirected_to :action =>'index', :project_id => 'ecookbook' |
|
1123 | ||
1124 |
assert session[:query] |
|
1125 |
assert session[:query][:column_names] |
|
1126 |
assert !session[:query][:column_names].include?(:status) |
|
1127 |
end |
|
1128 | ||
1097 | 1129 |
def test_destroy_routing |
1098 | 1130 |
assert_recognizes( #TODO: use DELETE on issue URI (need to change forms) |
1099 | 1131 |
{:controller => 'issues', :action => 'destroy', :id => '1'}, |
test/functional/queries_controller_test.rb | ||
---|---|---|
58 | 58 |
:disabled => nil } |
59 | 59 |
end |
60 | 60 |
|
61 |
def test_get_new_project_query_with_columns_from_session |
|
62 |
@request.session[:user_id] = 2 |
|
63 |
@request.session[:query] = {:column_names => ['tracker', 'subject']} |
|
64 |
get :new, :project_id => 1 |
|
65 |
assert_response :success |
|
66 |
assert_template 'new' |
|
67 | ||
68 |
assert_select '#query_default_columns[checked=checked]', false |
|
69 | ||
70 |
assert_select 'select#selected_columns' do |
|
71 |
assert_select 'option[value=tracker]' |
|
72 |
assert_select 'option[value=subject]' |
|
73 |
end |
|
74 |
end |
|
75 | ||
61 | 76 |
def test_new_project_public_query |
62 | 77 |
@request.session[:user_id] = 2 |
63 | 78 |
post :new, |