Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ jobs:
with:
path: plugins/redmine_issues_panel

- run: bin/rails redmine:plugins:migrate NAME=redmine_issues_panel RAILS_ENV=test

- run: bin/rails redmine:plugins:test NAME=redmine_issues_panel
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ This is a plugin for Redmine to display issues by statuses and change it's statu
$ git clone https://github.com/redmica/redmine_issues_panel.git /path/to/redmine/plugins/redmine_issues_panel
```

#### Migration (to add the new table)

```
$ bin/rails redmine:plugins:migrate NAME=redmine_issues_panel
```

## How to activate the Issues Panel

#### Check the 'Issues Panel' checkbox on the Project->Settings->Modules and save it.
Expand All @@ -35,6 +41,12 @@ $ bundle exec rake redmine:plugins:test NAME=redmine_issues_panel RAILS_ENV=test

## Uninstall

#### Run the migration to remove the table.

```
$ bin/rails redmine:plugins:migrate NAME=redmine_issues_panel VERSION=0
```

#### Remove the plugin directory.

```
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/issues_panel_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ def retrieve_issue_panel(params={})
elsif params[:query_id].blank? && session[session_key][:issues_num_per_row]
@query.issues_num_per_row = session[session_key][:issues_num_per_row]
end
if params[:set_filter] && params[:query] && params[:query][:enable_manual_ordering]
session[session_key][:enable_manual_ordering] = @query.enable_manual_ordering
elsif params[:query_id].blank? && session[session_key][:enable_manual_ordering]
@query.enable_manual_ordering = session[session_key][:enable_manual_ordering]
end
end
@issues_panel.query = @query
end
Expand Down
13 changes: 10 additions & 3 deletions app/models/issue_card.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class IssueCard < Issue
attribute :reordered, :boolean, default: false
validate do
(@move_attributes.keys - self.changes_to_save.keys).each do |attr|
self.errors.add attr, l('activerecord.errors.messages.can_t_be_changed')
Expand All @@ -23,9 +24,15 @@ def move!(attributes={})
end
end
end
if @move_attributes.any? || @custom_field_attributes.any?
self.safe_attributes = @move_attributes.merge(@custom_field_attributes)
self.save!
self.transaction do
if @move_attributes.any? || @custom_field_attributes.any?
self.safe_attributes = @move_attributes.merge(@custom_field_attributes)
self.save!
end
if attributes[:ordered_issue_ids].is_a?(Array) && attributes[:ordered_issue_ids].any?
IssueCardPosition.update_positions!(attributes[:ordered_issue_ids])
self.reordered = true
end
end
end

Expand Down
16 changes: 16 additions & 0 deletions app/models/issue_card_position.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class IssueCardPosition < ActiveRecord::Base
class << self
def update_positions!(ordered_issue_ids=[])
return if ordered_issue_ids.blank?
ordered_issue_positions = ordered_issue_ids.each_with_index.map { |id, i| { issue_id: id, position: i } }
opts = { update_only: %i[position] }
# :unique_by option is supported only by PostgreSQL and SQLite3
opts[:unique_by] = 'index_issue_card_positions_on_issue_id' if Redmine::Database.postgresql? || Redmine::Database.sqlite?
self.upsert_all(ordered_issue_positions, **opts)
end
end

def to_s
position.to_s
end
end
12 changes: 10 additions & 2 deletions app/views/issues_panel/_query_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,15 @@
<%= hidden_field_tag 'selected_query_issues_num_per_row', @query.issues_num_per_row %>
<%= select_tag 'query[issues_num_per_row]', options_for_select([1, 2, 3], @query.issues_num_per_row) %>
</div>
<div>
<div class="field"><%= l(:field_enable_manual_ordering) %></div>
<label for='query_enable_manual_ordering'>
<%= hidden_field_tag 'query[enable_manual_ordering]', '0' %>
<%= check_box_tag 'query[enable_manual_ordering]', '1', @query.enable_manual_ordering?, :onchange => 'toggleSortSelection()' %>
</label>
</div>
<% if @query.sortable_columns.any? %>
<div>
<div id="sort">
<div class="field"><label for='sort'><%= l(:label_sort) %></label></div>
<div>
<% 3.times do |i| %>
Expand Down Expand Up @@ -100,7 +107,8 @@ $(function ($) {
} else {
$('table#list-definition').hide();
}
})
});
toggleSortSelection();
});

<% end %>
51 changes: 40 additions & 11 deletions app/views/issues_panel/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ function loadDraggableSettings() {
});
});
}
function moveIssueCard(issue_card, receiver, ordered_issue_ids=[]) {
$.ajax({
url: '<%= move_issue_card_path(:format => 'js') %>',
type: 'put',
data: {
'id': issue_card.attr('data-issue-id'),
<%= @project ? ("'project_id': #{@project.id},").html_safe : '' %>
'status_id': receiver.attr('data-status-id'),
'group_key': receiver.attr('data-group-key'),
'group_value': receiver.attr('data-group-value'),
'ordered_issue_ids': ordered_issue_ids
}
});
}
function loadDroppableSetting() {
$(".issue-card-receiver").droppable({
accept: ".issue-card",
Expand All @@ -78,22 +92,33 @@ function loadDroppableSetting() {
(typeof(new_group_value) === "undefiend" || org_group_value==new_group_value)) {
return $(ui.draggable).addClass('drag-revert');
} else {
$.ajax({
url: '<%= move_issue_card_path(:format => 'js') %>',
type: 'put',
data: {
'id': ui.draggable.attr('data-issue-id'),
<%= @project ? ("'project_id': #{@project.id},").html_safe : '' %>
'status_id': $(this).attr('data-status-id'),
'group_key': $(this).attr('data-group-key'),
'group_value': $(this).attr('data-group-value')
}
});
moveIssueCard(ui.draggable, $(this));
}
}
}
});
}
function loadSortableSetting() {
$(".issue-card-receiver").sortable({
items: ".issue-card:not(.add-issue-card)",
connectWith: '.issue-card-receiver',
revert: true,
start: function(event, ui) {
ui.item.css({ opacity: 0.9 });
ui.placeholder.height(ui.item.height());
},
activate: function() { $('.hascontextmenu').removeClass('context-menu-selection cm-last');contextMenuHide();hideIssueDescription(); },
update: function(event, ui) {
var current_receiver = ui.item.parents('.issue-card-receiver')[0];
if (this !== current_receiver) return; // do nothing if the update event is not fired in the current receiver
var ordered_issue_ids = [];
$(this).find('.issue-card:not(.add-issue-card)').each(function(_, elem) {
ordered_issue_ids.push($(elem).attr('data-issue-id'));
});
moveIssueCard(ui.item, $(this), ordered_issue_ids);
}
}).disableSelection();
}
function showIssueDescription(issue_element, description_element) {
var issue_id = issue_element.attr('href').split('/').at(-1)
var mouse_x = issue_element.offset().left;
Expand Down Expand Up @@ -193,8 +218,12 @@ function loadCardFunctions(){
});
}
$(document).ready(function(){
<% if @query.enable_manual_ordering? %>
loadSortableSetting();
<% else %>
loadDraggableSettings();
loadDroppableSetting();
<% end %>
loadCardFunctions();
hideIssueDescription();
});
Expand Down
15 changes: 11 additions & 4 deletions app/views/issues_panel/move_issue_card.js.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% @issues_panel.view = self %>
<% if @issue_card.saved_changes? %>
<% if @issue_card.saved_changes? || @issue_card.reordered? %>
/// remove issue card
$('#issue-card-<%= @issue_card.id %>').remove();
// refresh status total
Expand All @@ -16,11 +16,18 @@
// restore background-color
contextMenuAddSelection($('#issue-<%= @issue_card.id %>'));
loadCardFunctions();
loadDraggableSettings();
<% unless @issues_panel.query.enable_manual_ordering? %>
loadDraggableSettings();
<% end %>
<% else %>
<% if flash[:error].present? %>
alert('<%= raw(escape_javascript(flash[:error])) %>');
<% end %>
// revart issue card
$('#issue-card-<%= @issue_card.id %>').animate( {left: 0, top: 0}, 500 );
<% if @issues_panel.query.enable_manual_ordering? %>
// cancel card reordering
$('.issue-card-receiver').sortable('cancel');
<% else %>
// revart issue card
$('#issue-card-<%= @issue_card.id %>').animate( {left: 0, top: 0}, 500 );
<% end %>
<% end %>
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ en:
notice_failed_to_move_issue: "Failed to move issue."
label_assign_to_me: "Assign to me"
field_issues_num_per_row: "Issues num per row"
field_issue_card_position: "Issue card position"
field_enable_manual_ordering: "Enable manual ordering"

project_module_issues_panel: "Issues Panel"

Expand Down
2 changes: 2 additions & 0 deletions config/locales/ja.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ ja:
notice_failed_to_move_issue: "チケットの移動に失敗しました。"
label_assign_to_me: "自分に割り当て"
field_issues_num_per_row: "一行ごとのチケット数"
field_issue_card_position: "カードの位置"
field_enable_manual_ordering: "手動で並び替え"

project_module_issues_panel: "チケットパネル"

Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20250901151155_create_issue_card_positions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateIssueCardPositions < ActiveRecord::Migration[7.2]
def change
create_table :issue_card_positions do |t|
t.column :issue_id, :integer, null: false
t.column :position, :integer, null: false, default: 0
t.column :created_on, :datetime, null: false
t.column :updated_on, :datetime, null: false
end
add_index :issue_card_positions, :issue_id, name: 'index_issue_card_positions_on_issue_id', unique: true
add_index :issue_card_positions, :position, name: 'index_issue_card_positions_on_position'
end
end
1 change: 1 addition & 0 deletions init.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require File.expand_path('../lib/redmine_issues_panel/queries_controller_patch', __FILE__)
require File.expand_path('../lib/redmine_issues_panel/view_hook', __FILE__)
require File.expand_path('../lib/redmine_issues_panel/issue_patch', __FILE__)
require File.expand_path('../lib/redmine_issues_panel/issue_query_patch', __FILE__)

Redmine::Plugin.register :redmine_issues_panel do
Expand Down
1 change: 1 addition & 0 deletions lib/redmine/helpers/issues_panel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def initialize(options={})
def query=(query)
@query = query
query.available_columns.delete_if { |c| c.name == :tracker }
query.use_on_issues_panel = true
@truncated = @query.issue_count.to_i > @issues_limit.to_i
end

Expand Down
13 changes: 13 additions & 0 deletions lib/redmine_issues_panel/issue_patch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'issue'

module RedmineIssuesPanel
module IssuePatch
def self.included(base)
base.class_eval do
has_one :issue_card_position, dependent: :destroy, foreign_key: :issue_id
end
end
end
end

Issue.include RedmineIssuesPanel::IssuePatch
45 changes: 45 additions & 0 deletions lib/redmine_issues_panel/issue_query_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ def self.included(base)
base.send(:prepend, InstanceMethods)
base.class_eval do
#unloadable
attribute :use_on_issues_panel, :boolean, default: false
self.available_columns << QueryColumn.new(:issue_card_position, :sortable => "#{IssueCardPosition.table_name}.position")
end
end

Expand All @@ -16,9 +18,35 @@ def build_from_params(params, defaults={})
params[:issues_num_per_row] ||
(params[:query] && params[:query][:issues_num_per_row]) ||
options[:issues_num_per_row]
self.enable_manual_ordering =
params[:enable_manual_ordering] ||
(params[:query] && params[:query][:enable_manual_ordering]) ||
options[:enable_manual_ordering]
self
end

def base_scope
s = super
if (self.use_on_issues_panel? && self.enable_manual_ordering?) ||
(self.column_names && self.column_names.include?(:issue_card_position))
s = s.includes(:issue_card_position)
end
s
end

def issues(options={})
if self.enable_manual_ordering?
options[:order] = []
if Redmine::Database.postgresql?
options[:order] << Arel.sql("#{IssueCardPosition.table_name}.position ASC NULLS FIRST")
else
options[:order] << Arel.sql("#{IssueCardPosition.table_name}.position ASC")
end
options[:order] << Arel.sql("#{Issue.table_name}.id DESC")
end
super(options)
end

def issues_num_per_row
r = options[:issues_num_per_row]
r.to_i == 0 ? 1 : r.to_i
Expand All @@ -27,6 +55,23 @@ def issues_num_per_row
def issues_num_per_row=(arg)
options[:issues_num_per_row] = arg ? arg.to_i : 1
end

def enable_manual_ordering
options[:enable_manual_ordering]
end

def enable_manual_ordering?
return false unless self.use_on_issues_panel?
if self.enable_manual_ordering.nil?
self.new_record? ? true : false
else
self.enable_manual_ordering.to_s == '1'
end
end

def enable_manual_ordering=(arg)
options[:enable_manual_ordering] = (arg.to_s == '0' ? '0' : '1')
end
end
end
end
Expand Down
17 changes: 17 additions & 0 deletions lib/redmine_issues_panel/view_hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@ class ViewHook < Redmine::Hook::ViewListener
def view_layouts_base_html_head(context={})
query = context[:controller].instance_variable_get(:'@query')
html = +''
js = <<~JS
function toggleSortSelection() {
if ($('input#query_enable_manual_ordering').is(':checked')) {
$('#sort').hide();
} else {
$('#sort').show();
}
}
JS
html << javascript_tag(js)
if (context[:controller] && context[:controller].is_a?(QueriesController)) &&
(context[:request] && context[:request].try(:params).is_a?(Hash) && context[:request].params['issues_panel'])
query.use_on_issues_panel = true
js = <<~JS
$(document).ready(function(){
var selector = '#{select_tag('query[issues_num_per_row]', options_for_select([1, 2, 3], (query && query.issues_num_per_row)).gsub("\n",'').html_safe)}';
$('form#query-form').append('#{hidden_field_tag('issues_panel', '1')}');
$('p.block_columns').remove();
$('p.totable_columns').remove();
$('p#group_by').after('<p id="issues_num_per_row"><label for="query_issues_num_per_row">#{l(:field_issues_num_per_row)}</label>' + selector + '</p>');
var checkbox = '#{hidden_field_tag('query[enable_manual_ordering]', 0, id: nil)}#{check_box_tag('query[enable_manual_ordering]', 1, (query && query.enable_manual_ordering?))}';
$('p#issues_num_per_row').after('<p id="enable_manual_ordering"><label for="query_enable_manual_ordering">#{l(:field_enable_manual_ordering)}</label>' + checkbox + '</p>');
toggleSortSelection();
$('input#query_enable_manual_ordering').on('change', function(){
toggleSortSelection();
});
});
JS
html << javascript_tag(js)
Expand Down
Loading