Feature #12909 » drag_drop_ordering_resource_trunk_r15195.patch
app/controllers/custom_fields_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class CustomFieldsController < ApplicationController |
19 | 19 |
layout 'admin' |
20 |
include Redmine::DragDropOrdering::Controller |
|
20 | 21 | |
21 | 22 |
before_filter :require_admin |
22 | 23 |
before_filter :build_new_custom_field, :only => [:new, :create] |
app/controllers/enumerations_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class EnumerationsController < ApplicationController |
19 | 19 |
layout 'admin' |
20 |
include Redmine::DragDropOrdering::Controller |
|
20 | 21 | |
21 | 22 |
before_filter :require_admin, :except => :index |
22 | 23 |
before_filter :require_admin_or_api_request, :only => :index |
app/controllers/issue_statuses_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class IssueStatusesController < ApplicationController |
19 | 19 |
layout 'admin' |
20 |
include Redmine::DragDropOrdering::Controller |
|
20 | 21 | |
21 | 22 |
before_filter :require_admin, :except => :index |
22 | 23 |
before_filter :require_admin_or_api_request, :only => :index |
app/controllers/roles_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class RolesController < ApplicationController |
19 | 19 |
layout 'admin' |
20 |
include Redmine::DragDropOrdering::Controller |
|
20 | 21 | |
21 | 22 |
before_filter :require_admin, :except => [:index, :show] |
22 | 23 |
before_filter :require_admin_or_api_request, :only => [:index, :show] |
app/controllers/trackers_controller.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class TrackersController < ApplicationController |
19 | 19 |
layout 'admin' |
20 |
include Redmine::DragDropOrdering::Controller |
|
20 | 21 | |
21 | 22 |
before_filter :require_admin, :except => :index |
22 | 23 |
before_filter :require_admin_or_api_request, :only => :index |
app/helpers/application_helper.rb | ||
---|---|---|
28 | 28 |
include Redmine::SudoMode::Helper |
29 | 29 |
include Redmine::Themes::Helper |
30 | 30 |
include Redmine::Hook::Helper |
31 |
include Redmine::DragDropOrdering::Helper |
|
31 | 32 | |
32 | 33 |
extend Forwardable |
33 | 34 |
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
app/models/custom_field.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class CustomField < ActiveRecord::Base |
19 | 19 |
include Redmine::SubclassFactory |
20 |
include Redmine::DragDropOrdering::Model |
|
20 | 21 | |
21 | 22 |
has_many :enumerations, |
22 | 23 |
lambda { order(:position) }, |
app/models/enumeration.rb | ||
---|---|---|
17 | 17 | |
18 | 18 |
class Enumeration < ActiveRecord::Base |
19 | 19 |
include Redmine::SubclassFactory |
20 |
include Redmine::DragDropOrdering::Model |
|
20 | 21 | |
21 | 22 |
default_scope lambda {order(:position)} |
22 | 23 |
app/models/issue_status.rb | ||
---|---|---|
16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 | |
18 | 18 |
class IssueStatus < ActiveRecord::Base |
19 |
include Redmine::DragDropOrdering::Model |
|
20 | ||
19 | 21 |
before_destroy :check_integrity |
20 | 22 |
has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" |
21 | 23 |
has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id" |
app/models/role.rb | ||
---|---|---|
16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 | |
18 | 18 |
class Role < ActiveRecord::Base |
19 |
include Redmine::DragDropOrdering::Model |
|
20 | ||
19 | 21 |
# Custom coder for the permissions attribute that should be an |
20 | 22 |
# array of symbols. Rails 3 uses Psych which can be *unbelievably* |
21 | 23 |
# slow on some platforms (eg. mingw32). |
app/models/tracker.rb | ||
---|---|---|
16 | 16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 | |
18 | 18 |
class Tracker < ActiveRecord::Base |
19 |
include Redmine::DragDropOrdering::Model |
|
19 | 20 | |
20 | 21 |
CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze |
21 | 22 |
# Fields that can be disabled |
app/views/custom_fields/_index.html.erb | ||
---|---|---|
1 |
<table class="list"> |
|
1 |
<%= form_tag reorder_all_custom_fields_path, :method => 'post', :id => 'sortable_form' do %> |
|
2 |
<table class="list" id="sortable_table"> |
|
2 | 3 |
<thead><tr> |
3 | 4 |
<th><%=l(:field_name)%></th> |
4 | 5 |
<th><%=l(:field_field_format)%></th> |
... | ... | |
14 | 15 |
<% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> |
15 | 16 |
<% back_url = custom_fields_path(:tab => tab[:name]) %> |
16 | 17 |
<tr class="<%= cycle("odd", "even") %>"> |
17 |
<td class="name"><%= link_to custom_field.name, edit_custom_field_path(custom_field) %></td> |
|
18 |
<td class="name"> |
|
19 |
<span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span> |
|
20 |
<%= hidden_field_tag "id_and_positions[#{custom_field.id}]", custom_field.position, :class => 'position' %> |
|
21 |
<%= link_to custom_field.name, edit_custom_field_path(custom_field) %> |
|
22 |
</td> |
|
18 | 23 |
<td><%= l(custom_field.format.label) %></td> |
19 | 24 |
<td><%= checked_image custom_field.is_required? %></td> |
20 | 25 |
<% if tab[:name] == 'IssueCustomField' %> |
... | ... | |
29 | 34 |
<% end; reset_cycle %> |
30 | 35 |
</tbody> |
31 | 36 |
</table> |
37 |
<% end %> |
|
38 | ||
39 |
<%= sortable_js('sortable_form', 'sortable_table') %> |
app/views/enumerations/index.html.erb | ||
---|---|---|
5 | 5 | |
6 | 6 |
<% enumerations = klass.shared %> |
7 | 7 |
<% if enumerations.any? %> |
8 |
<table class="list"><thead> |
|
8 |
<%= form_tag reorder_all_enumerations_path, :method => 'post', :id => "#{klass.name.underscore}_form" do %> |
|
9 |
<table class="list" id="<%= "#{klass.name.underscore}_table" %>"><thead> |
|
9 | 10 |
<tr> |
10 | 11 |
<th><%= l(:field_name) %></th> |
11 | 12 |
<th><%= l(:field_is_default) %></th> |
... | ... | |
15 | 16 |
</tr></thead> |
16 | 17 |
<% enumerations.each do |enumeration| %> |
17 | 18 |
<tr class="<%= cycle('odd', 'even') %>"> |
18 |
<td class="name"><%= link_to enumeration, edit_enumeration_path(enumeration) %></td> |
|
19 |
<td class="name"> |
|
20 |
<span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span> |
|
21 |
<%= hidden_field_tag "id_and_positions[#{enumeration.id}]", enumeration.position, :class => 'position' %> |
|
22 |
<%= link_to enumeration, edit_enumeration_path(enumeration) %> |
|
23 |
</td> |
|
19 | 24 |
<td class="tick"><%= checked_image enumeration.is_default? %></td> |
20 | 25 |
<td class="tick"><%= checked_image enumeration.active? %></td> |
21 | 26 |
<td class="reorder"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %></td> |
... | ... | |
23 | 28 |
</tr> |
24 | 29 |
<% end %> |
25 | 30 |
</table> |
31 |
<% end %> |
|
32 |
<%= sortable_js("#{klass.name.underscore}_form", "#{klass.name.underscore}_table") %> |
|
26 | 33 |
<% reset_cycle %> |
27 | 34 |
<% end %> |
28 | 35 |
app/views/issue_statuses/index.html.erb | ||
---|---|---|
5 | 5 | |
6 | 6 |
<h2><%=l(:label_issue_status_plural)%></h2> |
7 | 7 | |
8 |
<table class="list"> |
|
8 |
<%= form_tag reorder_all_issue_statuses_path, :method => 'post', :id => 'sortable_form' do %> |
|
9 |
<table class="list" id="sortable_table"> |
|
9 | 10 |
<thead><tr> |
10 | 11 |
<th><%=l(:field_status)%></th> |
11 | 12 |
<% if Issue.use_status_for_done_ratio? %> |
... | ... | |
18 | 19 |
<tbody> |
19 | 20 |
<% for status in @issue_statuses %> |
20 | 21 |
<tr class="<%= cycle("odd", "even") %>"> |
21 |
<td class="name"><%= link_to status.name, edit_issue_status_path(status) %></td> |
|
22 |
<td class="name"> |
|
23 |
<span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span> |
|
24 |
<%= hidden_field_tag "id_and_positions[#{status.id}]", status.position, :class => 'position' %> |
|
25 |
<%= link_to status.name, edit_issue_status_path(status) %> |
|
26 |
</td> |
|
22 | 27 |
<% if Issue.use_status_for_done_ratio? %> |
23 | 28 |
<td><%= status.default_done_ratio %></td> |
24 | 29 |
<% end %> |
... | ... | |
31 | 36 |
<% end %> |
32 | 37 |
</tbody> |
33 | 38 |
</table> |
39 |
<% end %> |
|
34 | 40 | |
35 | 41 |
<span class="pagination"><%= pagination_links_full @issue_status_pages %></span> |
36 | 42 | |
37 | 43 |
<% html_title(l(:label_issue_status_plural)) -%> |
44 |
<%= sortable_js('sortable_form', 'sortable_table') %> |
app/views/roles/index.html.erb | ||
---|---|---|
5 | 5 | |
6 | 6 |
<h2><%=l(:label_role_plural)%></h2> |
7 | 7 | |
8 |
<table class="list"> |
|
8 |
<%= form_tag reorder_all_roles_path, :method => 'post', :id => 'sortable_form' do %> |
|
9 |
<table class="list" id="sortable_table"> |
|
9 | 10 |
<thead><tr> |
10 | 11 |
<th><%=l(:label_role)%></th> |
11 | 12 |
<th><%=l(:button_sort)%></th> |
... | ... | |
14 | 15 |
<tbody> |
15 | 16 |
<% for role in @roles %> |
16 | 17 |
<tr class="<%= cycle("odd", "even") %>"> |
17 |
<td class="name"><%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %></td> |
|
18 |
<td class="name"> |
|
19 |
<% unless role.builtin? %> |
|
20 |
<span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span> |
|
21 |
<%= hidden_field_tag "id_and_positions[#{role.id}]", role.position, :class => 'position' %> |
|
22 |
<% end %> |
|
23 |
<%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %> |
|
24 |
</td> |
|
18 | 25 |
<td class="reorder"> |
19 | 26 |
<% unless role.builtin? %> |
20 | 27 |
<%= reorder_links('role', {:action => 'update', :id => role, :page => params[:page]}, :put) %> |
... | ... | |
28 | 35 |
<% end %> |
29 | 36 |
</tbody> |
30 | 37 |
</table> |
38 |
<% end %> |
|
31 | 39 | |
32 | 40 |
<span class="pagination"><%= pagination_links_full @role_pages %></span> |
33 | 41 | |
34 | 42 |
<% html_title(l(:label_role_plural)) -%> |
43 |
<%= sortable_js('sortable_form', 'sortable_table') %> |
app/views/trackers/index.html.erb | ||
---|---|---|
5 | 5 | |
6 | 6 |
<h2><%=l(:label_tracker_plural)%></h2> |
7 | 7 | |
8 |
<table class="list"> |
|
8 |
<%= form_tag reorder_all_trackers_path, :method => 'post', :id => 'sortable_form' do %> |
|
9 |
<table class="list" id="sortable_table"> |
|
9 | 10 |
<thead><tr> |
10 | 11 |
<th><%=l(:label_tracker)%></th> |
11 | 12 |
<th></th> |
... | ... | |
15 | 16 |
<tbody> |
16 | 17 |
<% for tracker in @trackers %> |
17 | 18 |
<tr class="<%= cycle("odd", "even") %>"> |
18 |
<td class="name"><%= link_to tracker.name, edit_tracker_path(tracker) %></td> |
|
19 |
<td class="name"> |
|
20 |
<span class="sort-handle ui-icon ui-icon-arrowthick-2-n-s"></span> |
|
21 |
<%= hidden_field_tag "id_and_positions[#{tracker.id}]", tracker.position, :class => 'position' %> |
|
22 |
<%= link_to tracker.name, edit_tracker_path(tracker) %> |
|
23 |
</td> |
|
19 | 24 |
<td> |
20 | 25 |
<% unless tracker.workflow_rules.count > 0 %> |
21 | 26 |
<span class="icon icon-warning"> |
... | ... | |
33 | 38 |
<% end %> |
34 | 39 |
</tbody> |
35 | 40 |
</table> |
41 |
<% end %> |
|
36 | 42 | |
37 | 43 |
<span class="pagination"><%= pagination_links_full @tracker_pages %></span> |
38 | 44 | |
39 | 45 |
<% html_title(l(:label_tracker_plural)) -%> |
46 |
<%= sortable_js('sortable_form', 'sortable_table') %> |
config/routes.rb | ||
---|---|---|
376 | 376 |
end |
377 | 377 |
end |
378 | 378 |
end |
379 |
extend Redmine::DragDropOrdering::Routes |
|
379 | 380 |
end |
lib/redmine.rb | ||
---|---|---|
63 | 63 |
require 'redmine/hook/listener' |
64 | 64 |
require 'redmine/hook/view_listener' |
65 | 65 |
require 'redmine/plugin' |
66 |
require 'redmine/drag_drop_ordering' |
|
66 | 67 | |
67 | 68 |
Redmine::Scm::Base.add "Subversion" |
68 | 69 |
Redmine::Scm::Base.add "Darcs" |
lib/redmine/drag_drop_ordering.rb | ||
---|---|---|
1 |
require 'active_support/concern' |
|
2 | ||
3 |
module Redmine |
|
4 |
module DragDropOrdering |
|
5 |
module Model |
|
6 |
extend ActiveSupport::Concern |
|
7 |
module ClassMethods |
|
8 |
def reorder_all(id_and_positions) |
|
9 |
return unless id_and_positions.is_a?(Hash) |
|
10 |
transaction do |
|
11 |
id_and_positions.each do |obj_id, position| |
|
12 |
obj = self.find(obj_id) |
|
13 |
obj.position = position.to_i |
|
14 |
unless obj.save |
|
15 |
raise ActiveRecord::Rollback |
|
16 |
end |
|
17 |
end |
|
18 |
end |
|
19 |
end |
|
20 |
end |
|
21 |
end |
|
22 | ||
23 |
module Controller |
|
24 |
def reorder_all |
|
25 |
model_klass = self.controller_name.classify.constantize |
|
26 |
model_klass.reorder_all(params[:id_and_positions]) |
|
27 |
redirect_to :action => :index |
|
28 |
end |
|
29 |
end |
|
30 | ||
31 |
module Routes |
|
32 |
def self.extended(routes) |
|
33 |
routes.instance_exec do |
|
34 |
concern :sortable do |
|
35 |
post :reorder_all, :on => :collection |
|
36 |
end |
|
37 |
resources :issue_statuses, :concerns => :sortable |
|
38 |
resources :custom_fields, :concerns => :sortable |
|
39 |
resources :enumerations, :concerns => :sortable |
|
40 |
resources :trackers, :concerns => :sortable |
|
41 |
resources :roles, :concerns => :sortable |
|
42 |
end |
|
43 |
end |
|
44 |
end |
|
45 | ||
46 |
module Helper |
|
47 |
def sortable_js(form_id, table_id) |
|
48 |
js = <<-"EOS" |
|
49 |
$(function() { |
|
50 |
$('##{table_id} tbody').sortable({ |
|
51 |
handle: '.sort-handle', |
|
52 |
update: function(event, ui) { |
|
53 |
$('##{table_id} tr').each(function(){ |
|
54 |
$(this).find('input.position').val($(this).index()+1); |
|
55 |
}); |
|
56 |
$('form##{form_id}').submit(); |
|
57 |
} |
|
58 |
}); |
|
59 |
}); |
|
60 |
EOS |
|
61 |
javascript_tag(js.html_safe) |
|
62 |
end |
|
63 |
end |
|
64 |
end |
|
65 |
end |
test/integration/lib/redmine/drag_drop_ordering_test.rb | ||
---|---|---|
1 |
# Redmine - project management software |
|
2 |
# Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or |
|
5 |
# modify it under the terms of the GNU General Public License |
|
6 |
# as published by the Free Software Foundation; either version 2 |
|
7 |
# of the License, or (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, write to the Free Software |
|
16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | ||
18 |
require File.expand_path('../../../../test_helper', __FILE__) |
|
19 | ||
20 |
class DragDropOrderingTest < Redmine::IntegrationTest |
|
21 |
|
|
22 |
fixtures :users, :custom_fields, :enumerations, :issue_statuses, :roles, :trackers |
|
23 |
|
|
24 |
def setup |
|
25 |
log_user("admin", "admin") |
|
26 |
end |
|
27 | ||
28 |
def test_reorder_all_uri |
|
29 |
resource_names = ['custom_fields', 'enumerations', 'issue_statuses', 'roles', 'trackers'] |
|
30 |
resource_names.each do |resource_name| |
|
31 |
# custom_fields --> CustomField |
|
32 |
model_klass = resource_name.classify.constantize |
|
33 |
desc_resource_ids = model_klass.all.order('position DESC').pluck(:id) |
|
34 |
new_positions = (1..desc_resource_ids.count).to_a |
|
35 |
# ex) {"3"=>"1", "2"=>"2", "1"=>"3"} |
|
36 |
id_and_positions = Hash[*desc_resource_ids.zip(new_positions).flatten] |
|
37 | ||
38 |
# ex) post "/custom_fields/reorder_all", {'id_and_positions' => {"3"=>"1","2"=>"2","3"=>"1"}} |
|
39 |
post "/#{resource_name}/reorder_all", {'id_and_positions' => id_and_positions} |
|
40 | ||
41 |
assert_redirected_to "/#{resource_name}" |
|
42 | ||
43 |
id_and_positions.each do |id, position| |
|
44 |
assert_equal position, model_klass.find(id).position |
|
45 |
end |
|
46 | ||
47 |
asc_resource_ids = model_klass.all.order('position ASC').pluck(:id) |
|
48 |
assert_equal asc_resource_ids, desc_resource_ids |
|
49 |
end |
|
50 |
end |
|
51 |
end |
test/integration/routing/reorder_all_test.rb | ||
---|---|---|
1 |
# Redmine - project management software |
|
2 |
# Copyright (C) 2006-2015 Jean-Philippe Lang |
|
3 |
# |
|
4 |
# This program is free software; you can redistribute it and/or |
|
5 |
# modify it under the terms of the GNU General Public License |
|
6 |
# as published by the Free Software Foundation; either version 2 |
|
7 |
# of the License, or (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU General Public License |
|
15 |
# along with this program; if not, write to the Free Software |
|
16 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | ||
18 |
require File.expand_path('../../../test_helper', __FILE__) |
|
19 | ||
20 |
class ReorderAllTest < Redmine::RoutingTest |
|
21 | ||
22 |
def test_reorder_all |
|
23 |
resource_names = ['custom_fields', 'enumerations', 'issue_statuses', 'roles', 'trackers'] |
|
24 |
resource_names.each do |resource_name| |
|
25 |
should_route "POST /#{resource_name}/reorder_all" => "#{resource_name}#reorder_all" |
|
26 |
end |
|
27 |
end |
|
28 |
end |