diff --git app/controllers/custom_fields_controller.rb app/controllers/custom_fields_controller.rb index aa9c6af..5482ff9 100644 --- app/controllers/custom_fields_controller.rb +++ app/controllers/custom_fields_controller.rb @@ -17,6 +17,7 @@ class CustomFieldsController < ApplicationController layout 'admin' + include Redmine::DragDropOrdering::Controller before_filter :require_admin before_filter :build_new_custom_field, :only => [:new, :create] diff --git app/controllers/enumerations_controller.rb app/controllers/enumerations_controller.rb index 5eba1fa..cd6cabe 100644 --- app/controllers/enumerations_controller.rb +++ app/controllers/enumerations_controller.rb @@ -17,6 +17,7 @@ class EnumerationsController < ApplicationController layout 'admin' + include Redmine::DragDropOrdering::Controller before_filter :require_admin, :except => :index before_filter :require_admin_or_api_request, :only => :index diff --git app/controllers/issue_statuses_controller.rb app/controllers/issue_statuses_controller.rb index 7b8103e..c320a19 100644 --- app/controllers/issue_statuses_controller.rb +++ app/controllers/issue_statuses_controller.rb @@ -17,6 +17,7 @@ class IssueStatusesController < ApplicationController layout 'admin' + include Redmine::DragDropOrdering::Controller before_filter :require_admin, :except => :index before_filter :require_admin_or_api_request, :only => :index diff --git app/controllers/roles_controller.rb app/controllers/roles_controller.rb index 33229cb..b5a33cb 100644 --- app/controllers/roles_controller.rb +++ app/controllers/roles_controller.rb @@ -17,6 +17,7 @@ class RolesController < ApplicationController layout 'admin' + include Redmine::DragDropOrdering::Controller before_filter :require_admin, :except => [:index, :show] before_filter :require_admin_or_api_request, :only => [:index, :show] diff --git app/controllers/trackers_controller.rb app/controllers/trackers_controller.rb index 4a291b5..28d0b1e 100644 --- app/controllers/trackers_controller.rb +++ app/controllers/trackers_controller.rb @@ -17,6 +17,7 @@ class TrackersController < ApplicationController layout 'admin' + include Redmine::DragDropOrdering::Controller before_filter :require_admin, :except => :index before_filter :require_admin_or_api_request, :only => :index diff --git app/helpers/application_helper.rb app/helpers/application_helper.rb index ff20064..09ec8b7 100644 --- app/helpers/application_helper.rb +++ app/helpers/application_helper.rb @@ -28,6 +28,7 @@ module ApplicationHelper include Redmine::SudoMode::Helper include Redmine::Themes::Helper include Redmine::Hook::Helper + include Redmine::DragDropOrdering::Helper extend Forwardable def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter diff --git app/models/custom_field.rb app/models/custom_field.rb index 8da7503..a3ae279 100644 --- app/models/custom_field.rb +++ app/models/custom_field.rb @@ -17,6 +17,7 @@ class CustomField < ActiveRecord::Base include Redmine::SubclassFactory + include Redmine::DragDropOrdering::Model has_many :enumerations, lambda { order(:position) }, diff --git app/models/enumeration.rb app/models/enumeration.rb index b2337fa..13211a4 100644 --- app/models/enumeration.rb +++ app/models/enumeration.rb @@ -17,6 +17,7 @@ class Enumeration < ActiveRecord::Base include Redmine::SubclassFactory + include Redmine::DragDropOrdering::Model default_scope lambda {order(:position)} diff --git app/models/issue_status.rb app/models/issue_status.rb index e39d69a..ab3cdda 100644 --- app/models/issue_status.rb +++ app/models/issue_status.rb @@ -16,6 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class IssueStatus < ActiveRecord::Base + include Redmine::DragDropOrdering::Model + before_destroy :check_integrity has_many :workflows, :class_name => 'WorkflowTransition', :foreign_key => "old_status_id" has_many :workflow_transitions_as_new_status, :class_name => 'WorkflowTransition', :foreign_key => "new_status_id" diff --git app/models/role.rb app/models/role.rb index 42dddaf..949bb98 100644 --- app/models/role.rb +++ app/models/role.rb @@ -16,6 +16,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Role < ActiveRecord::Base + include Redmine::DragDropOrdering::Model + # Custom coder for the permissions attribute that should be an # array of symbols. Rails 3 uses Psych which can be *unbelievably* # slow on some platforms (eg. mingw32). diff --git app/models/tracker.rb app/models/tracker.rb index cbf6bc0..5976fc7 100644 --- app/models/tracker.rb +++ app/models/tracker.rb @@ -16,6 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class Tracker < ActiveRecord::Base + include Redmine::DragDropOrdering::Model CORE_FIELDS_UNDISABLABLE = %w(project_id tracker_id subject description priority_id is_private).freeze # Fields that can be disabled diff --git app/views/custom_fields/_index.html.erb app/views/custom_fields/_index.html.erb index b0d3c27..6a586d8 100644 --- app/views/custom_fields/_index.html.erb +++ app/views/custom_fields/_index.html.erb @@ -1,4 +1,5 @@ - +<%= form_tag reorder_all_custom_fields_path, :method => 'post', :id => 'sortable_form' do %> +
@@ -14,7 +15,11 @@ <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%> <% back_url = custom_fields_path(:tab => tab[:name]) %> "> - + <% if tab[:name] == 'IssueCustomField' %> @@ -29,3 +34,6 @@ <% end; reset_cycle %>
<%=l(:field_name)%> <%=l(:field_field_format)%>
<%= link_to custom_field.name, edit_custom_field_path(custom_field) %> + + <%= hidden_field_tag "id_and_positions[#{custom_field.id}]", custom_field.position, :class => 'position' %> + <%= link_to custom_field.name, edit_custom_field_path(custom_field) %> + <%= l(custom_field.format.label) %> <%= checked_image custom_field.is_required? %>
+<% end %> + +<%= sortable_js('sortable_form', 'sortable_table') %> diff --git app/views/enumerations/index.html.erb app/views/enumerations/index.html.erb index d1fb919..6029880 100644 --- app/views/enumerations/index.html.erb +++ app/views/enumerations/index.html.erb @@ -5,7 +5,8 @@ <% enumerations = klass.shared %> <% if enumerations.any? %> - +<%= form_tag reorder_all_enumerations_path, :method => 'post', :id => "#{klass.name.underscore}_form" do %> +
"> @@ -15,7 +16,11 @@ <% enumerations.each do |enumeration| %> - + @@ -23,6 +28,8 @@ <% end %>
<%= l(:field_name) %> <%= l(:field_is_default) %>
<%= link_to enumeration, edit_enumeration_path(enumeration) %> + + <%= hidden_field_tag "id_and_positions[#{enumeration.id}]", enumeration.position, :class => 'position' %> + <%= link_to enumeration, edit_enumeration_path(enumeration) %> + <%= checked_image enumeration.is_default? %> <%= checked_image enumeration.active? %> <%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %>
+<% end %> +<%= sortable_js("#{klass.name.underscore}_form", "#{klass.name.underscore}_table") %> <% reset_cycle %> <% end %> diff --git app/views/issue_statuses/index.html.erb app/views/issue_statuses/index.html.erb index 40a5c92..220a978 100644 --- app/views/issue_statuses/index.html.erb +++ app/views/issue_statuses/index.html.erb @@ -5,7 +5,8 @@

<%=l(:label_issue_status_plural)%>

- +<%= form_tag reorder_all_issue_statuses_path, :method => 'post', :id => 'sortable_form' do %> +
<% if Issue.use_status_for_done_ratio? %> @@ -18,7 +19,11 @@ <% for status in @issue_statuses %> "> - + <% if Issue.use_status_for_done_ratio? %> <% end %> @@ -31,7 +36,9 @@ <% end %>
<%=l(:field_status)%>
<%= link_to status.name, edit_issue_status_path(status) %> + + <%= hidden_field_tag "id_and_positions[#{status.id}]", status.position, :class => 'position' %> + <%= link_to status.name, edit_issue_status_path(status) %> + <%= status.default_done_ratio %>
+<% end %> <%= pagination_links_full @issue_status_pages %> <% html_title(l(:label_issue_status_plural)) -%> +<%= sortable_js('sortable_form', 'sortable_table') %> diff --git app/views/roles/index.html.erb app/views/roles/index.html.erb index 6561d11..ff35351 100644 --- app/views/roles/index.html.erb +++ app/views/roles/index.html.erb @@ -5,7 +5,8 @@

<%=l(:label_role_plural)%>

- +<%= form_tag reorder_all_roles_path, :method => 'post', :id => 'sortable_form' do %> +
@@ -14,7 +15,13 @@ <% for role in @roles %> "> - +
<%=l(:label_role)%> <%=l(:button_sort)%>
<%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %> + <% unless role.builtin? %> + + <%= hidden_field_tag "id_and_positions[#{role.id}]", role.position, :class => 'position' %> + <% end %> + <%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %> + <% unless role.builtin? %> <%= reorder_links('role', {:action => 'update', :id => role, :page => params[:page]}, :put) %> @@ -28,7 +35,9 @@ <% end %>
+<% end %> <%= pagination_links_full @role_pages %> <% html_title(l(:label_role_plural)) -%> +<%= sortable_js('sortable_form', 'sortable_table') %> diff --git app/views/trackers/index.html.erb app/views/trackers/index.html.erb index 8ba7b7c..e1ce311 100644 --- app/views/trackers/index.html.erb +++ app/views/trackers/index.html.erb @@ -5,7 +5,8 @@

<%=l(:label_tracker_plural)%>

- +<%= form_tag reorder_all_trackers_path, :method => 'post', :id => 'sortable_form' do %> +
@@ -15,7 +16,11 @@ <% for tracker in @trackers %> "> - +
<%=l(:label_tracker)%>
<%= link_to tracker.name, edit_tracker_path(tracker) %> + + <%= hidden_field_tag "id_and_positions[#{tracker.id}]", tracker.position, :class => 'position' %> + <%= link_to tracker.name, edit_tracker_path(tracker) %> + <% unless tracker.workflow_rules.count > 0 %> @@ -33,7 +38,9 @@ <% end %>
+<% end %> <%= pagination_links_full @tracker_pages %> <% html_title(l(:label_tracker_plural)) -%> +<%= sortable_js('sortable_form', 'sortable_table') %> diff --git config/routes.rb config/routes.rb index 3b469d7..fa057aa 100644 --- config/routes.rb +++ config/routes.rb @@ -376,4 +376,5 @@ Rails.application.routes.draw do end end end + extend Redmine::DragDropOrdering::Routes end diff --git lib/redmine.rb lib/redmine.rb index d797a01..e685274 100644 --- lib/redmine.rb +++ lib/redmine.rb @@ -63,6 +63,7 @@ require 'redmine/hook' require 'redmine/hook/listener' require 'redmine/hook/view_listener' require 'redmine/plugin' +require 'redmine/drag_drop_ordering' Redmine::Scm::Base.add "Subversion" Redmine::Scm::Base.add "Darcs" diff --git lib/redmine/drag_drop_ordering.rb lib/redmine/drag_drop_ordering.rb new file mode 100644 index 0000000..395a83c --- /dev/null +++ lib/redmine/drag_drop_ordering.rb @@ -0,0 +1,65 @@ +require 'active_support/concern' + +module Redmine + module DragDropOrdering + module Model + extend ActiveSupport::Concern + module ClassMethods + def reorder_all(id_and_positions) + return unless id_and_positions.is_a?(Hash) + transaction do + id_and_positions.each do |obj_id, position| + obj = self.find(obj_id) + obj.position = position.to_i + unless obj.save + raise ActiveRecord::Rollback + end + end + end + end + end + end + + module Controller + def reorder_all + model_klass = self.controller_name.classify.constantize + model_klass.reorder_all(params[:id_and_positions]) + redirect_to :action => :index + end + end + + module Routes + def self.extended(routes) + routes.instance_exec do + concern :sortable do + post :reorder_all, :on => :collection + end + resources :issue_statuses, :concerns => :sortable + resources :custom_fields, :concerns => :sortable + resources :enumerations, :concerns => :sortable + resources :trackers, :concerns => :sortable + resources :roles, :concerns => :sortable + end + end + end + + module Helper + def sortable_js(form_id, table_id) + js = <<-"EOS" + $(function() { + $('##{table_id} tbody').sortable({ + handle: '.sort-handle', + update: function(event, ui) { + $('##{table_id} tr').each(function(){ + $(this).find('input.position').val($(this).index()+1); + }); + $('form##{form_id}').submit(); + } + }); + }); + EOS + javascript_tag(js.html_safe) + end + end + end +end diff --git test/integration/lib/redmine/drag_drop_ordering_test.rb test/integration/lib/redmine/drag_drop_ordering_test.rb new file mode 100644 index 0000000..cc47a2a --- /dev/null +++ test/integration/lib/redmine/drag_drop_ordering_test.rb @@ -0,0 +1,51 @@ +# Redmine - project management software +# Copyright (C) 2006-2015 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../../test_helper', __FILE__) + +class DragDropOrderingTest < Redmine::IntegrationTest + + fixtures :users, :custom_fields, :enumerations, :issue_statuses, :roles, :trackers + + def setup + log_user("admin", "admin") + end + + def test_reorder_all_uri + resource_names = ['custom_fields', 'enumerations', 'issue_statuses', 'roles', 'trackers'] + resource_names.each do |resource_name| + # custom_fields --> CustomField + model_klass = resource_name.classify.constantize + desc_resource_ids = model_klass.all.order('position DESC').pluck(:id) + new_positions = (1..desc_resource_ids.count).to_a + # ex) {"3"=>"1", "2"=>"2", "1"=>"3"} + id_and_positions = Hash[*desc_resource_ids.zip(new_positions).flatten] + + # ex) post "/custom_fields/reorder_all", {'id_and_positions' => {"3"=>"1","2"=>"2","3"=>"1"}} + post "/#{resource_name}/reorder_all", {'id_and_positions' => id_and_positions} + + assert_redirected_to "/#{resource_name}" + + id_and_positions.each do |id, position| + assert_equal position, model_klass.find(id).position + end + + asc_resource_ids = model_klass.all.order('position ASC').pluck(:id) + assert_equal asc_resource_ids, desc_resource_ids + end + end +end diff --git test/integration/routing/reorder_all_test.rb test/integration/routing/reorder_all_test.rb new file mode 100644 index 0000000..aa8be5c --- /dev/null +++ test/integration/routing/reorder_all_test.rb @@ -0,0 +1,28 @@ +# Redmine - project management software +# Copyright (C) 2006-2015 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require File.expand_path('../../../test_helper', __FILE__) + +class ReorderAllTest < Redmine::RoutingTest + + def test_reorder_all + resource_names = ['custom_fields', 'enumerations', 'issue_statuses', 'roles', 'trackers'] + resource_names.each do |resource_name| + should_route "POST /#{resource_name}/reorder_all" => "#{resource_name}#reorder_all" + end + end +end