Commit 83f809922710467935bf6b3024d23744adefd88a

Authored by Austin Smith
1 parent cb7737f9fb
Exists in master

initial load of alpha version

Showing 22 changed files with 811 additions and 0 deletions Side-by-side Diff

  1 += redmine_task_board
  2 +
  3 +Description goes here
app/controllers/taskboard_controller.rb View file @ 83f8099
  1 +class TaskboardController < ApplicationController
  2 + unloadable
  3 +
  4 + before_filter :find_project
  5 + before_filter :authorize
  6 + helper_method :column_manager_locals
  7 +
  8 + def index
  9 + @columns = TaskBoardColumn.find_all_by_project_id(@project.id, :order => 'weight')
  10 + @status_names = Hash.new
  11 + IssueStatus.select([:id, :name]).each do |status|
  12 + @status_names[status.id] = status.name
  13 + end
  14 + end
  15 +
  16 + def save
  17 + params[:sort].each do |status_id, issues|
  18 + weight = 0;
  19 + issues.each do |issue_id|
  20 + tbi = TaskBoardIssue.find_by_issue_id(issue_id).update_attribute(:project_weight, weight)
  21 + weight += 1
  22 + end
  23 + end
  24 + if params[:move] then
  25 + params[:move].each do |issue_id, new_status_id|
  26 + issue = Issue.find(issue_id).update_attribute(:status_id, new_status_id)
  27 + end
  28 + end
  29 + respond_to do |format|
  30 + format.js{ head :ok }
  31 + end
  32 + end
  33 +
  34 + def archive_issues
  35 + params[:ids].each do |issue_id|
  36 + TaskBoardIssue.find_by_issue_id(issue_id).update_attribute(:is_archived, true)
  37 + end
  38 + respond_to do |format|
  39 + format.js{ head :ok }
  40 + end
  41 + end
  42 +
  43 + def unarchive_issue
  44 + TaskBoardIssue.find_by_issue_id(params[:issue_id]).update_attribute(:is_archived, false)
  45 + respond_to do |format|
  46 + format.js{ head :ok }
  47 + end
  48 + end
  49 +
  50 + def create_column
  51 + @column = TaskBoardColumn.new :project => @project, :title => params[:title]
  52 + respond_to do |format|
  53 + if @column.save
  54 + format.js{ render :update do |page|
  55 + page.replace_html 'column_manager',
  56 + :partial => 'settings/column_manager',
  57 + :locals => {
  58 + :columns => TaskBoardColumn.find_all_by_project_id(@project.id),
  59 + :statuses => IssueStatus.all
  60 + }
  61 + end}
  62 + else
  63 + format.js{ render :update do |page|
  64 + page.replace_html 'column_manager', :label_task_board_application_error
  65 + end}
  66 + end
  67 + end
  68 + end
  69 +
  70 + def delete_column
  71 + @column = TaskBoardColumn.find(params[:column_id])
  72 + respond_to do |format|
  73 + if @column.delete
  74 + format.js{ render :update do |page|
  75 + page.replace_html 'column_manager',
  76 + :partial => 'settings/column_manager',
  77 + :locals => {
  78 + :columns => TaskBoardColumn.find_all_by_project_id(@project.id, :order => "weight ASC"),
  79 + :statuses => IssueStatus.all
  80 + }
  81 + end}
  82 + else
  83 + format.js{ render :update do |page|
  84 + page.replace_html 'column_manager', :label_task_board_application_error
  85 + end}
  86 + end
  87 + end
  88 + end
  89 +
  90 + def update_columns
  91 + params[:column].each do |column_id, new_state|
  92 + @column = TaskBoardColumn.find(column_id.to_i)
  93 + @column.weight = new_state[:weight].to_i
  94 + @column.max_issues = new_state[:max_issues].to_i
  95 + @column.save
  96 + @column.issue_statuses.clear()
  97 + end
  98 + params[:status].each do |status_id, column_id|
  99 + status_id = status_id.to_i
  100 + column_id = column_id.to_i
  101 + unless column_id == 0
  102 + @column = TaskBoardColumn.find(column_id)
  103 + @column.issue_statuses << IssueStatus.find(status_id)
  104 + end
  105 + end
  106 + respond_to do |format|
  107 + format.js{ render :update do |page|
  108 + page.replace_html 'column_manager',
  109 + :partial => 'settings/column_manager',
  110 + :locals => {
  111 + :columns => TaskBoardColumn.find_all_by_project_id(@project.id, :order => "weight ASC"),
  112 + :statuses => IssueStatus.all
  113 + }
  114 + end}
  115 + end
  116 + end
  117 +
  118 + private
  119 +
  120 + def find_project
  121 + # @project variable must be set before calling the authorize filter
  122 + if (params[:project_id]) then
  123 + @project = Project.find(params[:project_id])
  124 + elsif(params[:issue_id]) then
  125 + @project = Issue.find(params[:issue_id]).project
  126 + end
  127 + end
  128 +
  129 +end
app/helpers/taskboard_helper.rb View file @ 83f8099
  1 +module TaskboardHelper
  2 +end
app/models/task_board_column.rb View file @ 83f8099
  1 +class TaskBoardColumn < ActiveRecord::Base
  2 + unloadable
  3 + belongs_to :project
  4 + has_and_belongs_to_many :issue_statuses
  5 + validates_presence_of :title, :project
  6 + validates_length_of :title, :maximum => 255
  7 +
  8 + def self.empty_status(status_id)
  9 + columns = TaskBoardColumn
  10 + .select(:id)
  11 + .joins('INNER JOIN issue_statuses_task_board_columns istbc ON istbc.task_board_column_id = task_board_columns.id')
  12 + .where('istbc.issue_status_id = ?', status_id)
  13 + puts columns
  14 + return columns.empty?
  15 + end
  16 +
  17 + def issues(order_column="project_weight")
  18 + @column_statuses = Hash.new
  19 + self.issue_statuses.order(:name).each do |status|
  20 + @column_statuses[status.id] = Array.new
  21 + issues = Issue.select("issues.*, tbi.is_archived, tbi.#{order_column} as weight, tbi.issue_id")
  22 + .joins('LEFT OUTER JOIN task_board_issues AS tbi ON tbi.issue_id = issues.id')
  23 + .where("project_id = ? AND status_id = ? AND (is_archived IS NULL OR is_archived = 0)", self.project_id, status.id)
  24 + .order("weight ASC, created_on ASC")
  25 + issues.each do |issue|
  26 + # Create a TaskBoardIssue (i.e. a Card) if one doesn't exist already.
  27 + unless issue.issue_id
  28 + closed_and_old = (status.is_closed? and issue.updated_on.to_date < 14.days.ago.to_date)
  29 + tbi = TaskBoardIssue.new(:issue_id => issue.id, :is_archived => closed_and_old)
  30 + tbi.save
  31 + if closed_and_old
  32 + next
  33 + end
  34 + end
  35 + @column_statuses[status.id] << issue
  36 + end
  37 + end
  38 + return @column_statuses
  39 + end
  40 +
  41 +end
app/models/task_board_issue.rb View file @ 83f8099
  1 +class TaskBoardIssue < ActiveRecord::Base
  2 + unloadable
  3 + belongs_to :issue
  4 +end
app/views/settings/_column_manager.html.erb View file @ 83f8099
  1 +<%= form_tag project_taskboard_columns_create_path(:project_id => @project.id), :remote => true, :id => 'create-column' do %>
  2 + <p>
  3 + <%= label_tag(:title, translate(:task_board_column_title)) %>
  4 + <%= text_field_tag(:title) %>
  5 + <%= submit_tag(translate :task_board_create_column) %>
  6 + </p>
  7 +<% end %>
  8 +
  9 +<%= form_tag project_taskboard_columns_update_path(:project_id => @project.id), :method => 'put', :remote => true do %>
  10 + <ul class="cant-move">
  11 + <li class="head"><p class="cell"><%= translate :task_board_statuses %></p></li>
  12 + <% statuses.each do |issue_status| %>
  13 + <li><%= issue_status.name %></li>
  14 + <% end %>
  15 + </ul>
  16 +
  17 + <ul class="cant-move">
  18 + <li class="head"><p class="cell"><%= translate :task_board_not_shown %></p></li>
  19 + <% statuses.each do |issue_status| %>
  20 + <li class="has-radio">
  21 + <%= radio_button_tag("status[#{issue_status.id}]", 0, TaskBoardColumn.empty_status(issue_status.id)) %>
  22 + </li>
  23 + <% end %>
  24 + </ul>
  25 +
  26 + <div class="movable-columns" id="taskboard-columns">
  27 + <h4><%= translate :task_board_visible_columns %></h4>
  28 + <% columns.each do |column| %>
  29 + <ul>
  30 + <li class="head"><p class="cell"><%= column.title %></p></li>
  31 + <% statuses.each do |issue_status| %>
  32 + <li class="has-radio">
  33 + <%= radio_button_tag("status[#{issue_status.id}]", column.id, column.issue_status_ids.include?(issue_status.id)) %>
  34 + </li>
  35 + <% end %>
  36 + <li><%= link_to(
  37 + l(:button_delete),
  38 + project_taskboard_columns_delete_path(:project_id => @project.id, :column_id => column.id),
  39 + :method => :delete,
  40 + :confirm => l(:text_are_you_sure),
  41 + :remote => true,
  42 + :class => 'icon icon-del'
  43 + )
  44 + %>
  45 + </li>
  46 + <li class="column-options">
  47 + <p class="cell">
  48 + <%= label_tag("column[#{column.id}][max_issues]", translate(:task_board_task_limit)) %>
  49 + <%= text_field_tag("column[#{column.id}][max_issues]", column.max_issues, :class => 'column-max-issues') %>
  50 + <%= hidden_field_tag("column[#{column.id}][weight]", column.weight, :class => 'column-weight') %>
  51 + </p>
  52 + </li>
  53 + </ul>
  54 + <% end %>
  55 + </div>
  56 +
  57 + <div class="form-save">
  58 + <%= submit_tag(translate :task_board_save_changes) %>
  59 + </div>
  60 +
  61 + <script type="text/javascript">
  62 + var taskboard_settings = new TaskBoardSettings('taskboard-columns', {constraint: false, tag: 'ul', handle: 'head', weightSelector: '.column-options .column-weight', overlap: 'horizontal'});
  63 + </script>
  64 +<% end %>
app/views/settings/_project.html.erb View file @ 83f8099
  1 +<%= javascript_include_tag('task_board', :plugin => 'redmine_task_board') %>
  2 +
  3 +<style>
  4 +#column_manager { overflow: auto }
  5 +#column_manager ul { float: left; width: 110px; overflow: hidden; list-style-type: none; padding: 5px 0; margin: 0 3px 0 0; }
  6 +#column_manager div.movable-columns { float: left; }
  7 +#column_manager div.movable-columns h4 { text-align: center; height: 15px; padding: 5px 0; margin: 0 0 5px 0; font-size: 15px; }
  8 +#column_manager div.movable-columns ul { background: #ffd; }
  9 +#column_manager div.movable-columns ul li.head { cursor: move; }
  10 +#column_manager ul li { height: 22px; padding: 2px 4px 0 4px; width: 102px; text-align: center; }
  11 +#column_manager ul li p.cell { display: table-cell; vertical-align: middle; height: 24px; text-align: center; width: 102px; padding: 0; margin: 0; }
  12 +#column_manager ul li.head, #column_manager ul li.head p.cell { font-weight: bold; height: 60px; }
  13 +#column_manager ul li:nth-child(even) { background: #ededed; }
  14 +#column_manager ul li.column-options .column-max-issues { font-size: 11px; width: 22px; padding: 0; text-align: center; }
  15 +#column_manager div.movable-columns ul li:nth-child(even) { background: #ff9; }
  16 +#column_manager ul.cant-move { margin-top: 30px; }
  17 +#column_manager div.form-save { clear: left; padding-top: 15px; }
  18 +</style>
  19 +
  20 +<%= translate :task_board_help %>
  21 +
  22 +<div id="column_manager">
  23 + <%= render :partial => 'settings/column_manager', :locals => {
  24 + :columns => TaskBoardColumn.find_all_by_project_id(@project.id, :order => "weight ASC"),
  25 + :statuses => IssueStatus.all
  26 + }
  27 + %>
  28 +</div>
app/views/taskboard/_issue_description.html.erb View file @ 83f8099
  1 +<% if tbi = TaskBoardIssue.find_by_issue_id(@issue.id) and tbi.is_archived? then %>
  2 + <div id="taskboard-issue">
  3 + <hr>
  4 + <p><strong><%= translate :label_task_board %></strong></p>
  5 + <p>
  6 + <%= translate :task_board_issue_archived %>
  7 + <input type="button" id="taskboard-unarchive" value="<%= translate :task_board_issue_unarchive %>" />
  8 + </p>
  9 + </div>
  10 +
  11 + <script type="text/javascript">
  12 + $('taskboard-unarchive').observe('click', function() {
  13 + new Ajax.Request('/issues/<%= @issue.id %>/taskboard-unarchive', {
  14 + method: 'post',
  15 + onLoading: function() {
  16 + $('ajax-indicator').show();
  17 + },
  18 + onComplete: function() {
  19 + $('ajax-indicator').hide();
  20 + },
  21 + onSuccess: function() {
  22 + $('taskboard-issue').remove();
  23 + }
  24 + });
  25 + });
  26 + </script>
  27 +<% end %>
app/views/taskboard/index.html.erb View file @ 83f8099
  1 +<%= javascript_include_tag('task_board', :plugin => 'redmine_task_board') %>
  2 +
  3 +<div id="taskboard-buttons">
  4 + <input type="button" id="edit-issues" value="<%= translate :task_board_issue_bulk_edit %>" />
  5 + <input type="button" id="archive-issues" value="<%= translate :task_board_issue_bulk_archive %>" />
  6 +</div>
  7 +
  8 +<div class="taskboard-wrapper" id="sortable-root">
  9 + <% @columns.each do|column| %>
  10 + <div class="taskboard-pane">
  11 + <h2><%= column.title %></h2>
  12 + <% column.issues.each do |status_id, issues| %>
  13 + <% unless column.issues.size == 1 %>
  14 + <h3 class="status"><%= @status_names[status_id] %></h3>
  15 + <% end %>
  16 + <ul data-status-id="<%= status_id %>" data-max-issues="<%= column.max_issues %>" id="column_<%= status_id %>"<% if issues.empty? %> class="empty"<% end %>>
  17 + <% issues.each do|issue| %>
  18 + <li class="card <%= issue.tracker.name.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '') %>" id="issue_<%= issue.id %>" data-issue-id="<%= issue.id %>" data-weight="<%= issue.weight %>" data-status-id="<%= issue.status_id %>">
  19 + <div class="issue">
  20 + <div class="issue-heading">
  21 + <p class="issue-number meta">
  22 + <input type="checkbox" name="ids[]" value="<%= issue.id.to_s %>" />
  23 + <%= link_to "#{issue.tracker} ##{issue.id.to_s}", :controller => :issues, :action => :show, :id => issue.id %>
  24 + </p>
  25 + </div>
  26 + <h3><%= link_to issue.subject, :controller => :issues, :action => :show, :id => issue.id %></h3>
  27 + <% if defined? issue.assigned_to.name %>
  28 + <p class="meta">
  29 + <%= link_to issue.assigned_to.name, :controller => :users, :action => :show, :id => issue.assigned_to.id %>
  30 + </p>
  31 + <% end %>
  32 + </div>
  33 + </li>
  34 + <% end %>
  35 + </ul>
  36 + <% end %>
  37 + </div>
  38 + <% end %>
  39 +</div>
  40 +
  41 +<script type="text/javascript">
  42 + var project_save_url = '/projects/<%= @project.id %>/taskboard/save';
  43 + var project_archive_url = '/projects/<%= @project.id %>/taskboard/archive-issues';
  44 + var sections = [];
  45 + $$('#sortable-root .taskboard-pane ul').each(function(el) {
  46 + sections.push(el);
  47 + });
  48 + sections.each(function(el) {
  49 + new TaskBoardPane(el.id, {constraint: false, containment: sections, dropOnEmpty: true, only: 'card'});
  50 + });
  51 + TaskBoardUtils.checkboxListener();
  52 + // Sortable.create('sortable-root', {tree: true, dropOnEmpty: true, constraint: false, overlap: 'vertical'});
  53 +</script>
  54 +
  55 +<% content_for :header_tags do %>
  56 + <%= stylesheet_link_tag 'taskboard', :plugin => 'redmine_task_board' %>
  57 +<% end %>
assets/javascripts/task_board.js View file @ 83f8099
  1 +var TaskBoardSortable = Class.create({
  2 +
  3 + sortable: null,
  4 + id: null,
  5 + options: {},
  6 +
  7 + initialize: function(id, options) {
  8 + this.id = id;
  9 + this.options = options;
  10 + this.options.onChange = this.onChange.bind(this);
  11 + this.options.onUpdate = this.onUpdate.bind(this);
  12 + Sortable.create(id, this.options);
  13 + },
  14 +
  15 + onChange: function() { },
  16 +
  17 + onUpdate: function() { }
  18 +
  19 +});
  20 +
  21 +var TaskBoardPane = Class.create(TaskBoardSortable, {
  22 +
  23 + initialize: function($super, id, options) {
  24 + $super(id, options);
  25 + this.max_issues = parseInt($(id).readAttribute('data-max-issues'));
  26 + $(this.id).writeAttribute('data-card-count', this.getNumberOfCards());
  27 + },
  28 +
  29 + getNumberOfCards: function() {
  30 + return $(this.id).select('.card').length;
  31 + },
  32 +
  33 + onUpdate: function(list) {
  34 + // Add or remove 'empty' class
  35 + if (list.hasClassName('empty') && list.descendants().length > 0) {
  36 + list.removeClassName('empty');
  37 + }
  38 + else if (list.descendants().length == 0) {
  39 + list.addClassName('empty');
  40 + }
  41 +
  42 + // Deal with max issue limit
  43 + if (this.max_issues > 0 && $(this.id).childElements().length > this.max_issues) {
  44 + var i = 1;
  45 + $(this.id).childElements().each((function(card) {
  46 + // Clear legal cards of the over-limit class
  47 + if (i <= this.max_issues) {
  48 + card.removeClassName('over-limit');
  49 + }
  50 +
  51 + // Add a dashed line under the last legal issue, reset others
  52 + if (this.max_issues == i) {
  53 + card.addClassName('at-limit');
  54 + }
  55 + else {
  56 + card.removeClassName('at-limit');
  57 + }
  58 +
  59 + // Add over-limit class to over-limit issues
  60 + if (i > this.max_issues) {
  61 + console.log('over limit');
  62 + card.addClassName('over-limit');
  63 + }
  64 + i++;
  65 + }).bind(this));
  66 + }
  67 + else {
  68 + $(this.id).childElements().each(function(card) {
  69 + card.removeClassName('over-limit');
  70 + card.removeClassName('at-limit');
  71 + });
  72 + }
  73 +
  74 + // handle card movements
  75 +
  76 + // a card has been moved into this column.
  77 + if (this.getNumberOfCards() > $(this.id).readAttribute('data-card-count')) {
  78 + list.childElements().each(function(card) {
  79 + if (list.readAttribute('data-status-id') != card.readAttribute('data-status-id')) {
  80 + TaskBoardUtils.save([
  81 + TaskBoardUtils.serialize($(list.id)), // save ordering of this column
  82 + TaskBoardUtils.serialize($('column_' + card.readAttribute('data-status-id'))), // save ordering of previous column
  83 + TaskBoardUtils.moveParam(card.readAttribute('data-issue-id'), list.readAttribute('data-status-id'))
  84 + ], {
  85 + onSuccess: function() {
  86 + card.writeAttribute('data-status-id', list.readAttribute('data-status-id'));
  87 + }
  88 + });
  89 + }
  90 + });
  91 + }
  92 +
  93 + // this column has been reordered
  94 + else if(this.getNumberOfCards() == $(this.id).readAttribute('data-card-count')) {
  95 + TaskBoardUtils.save([TaskBoardUtils.serialize($(list.id))]);
  96 + }
  97 +
  98 + // We don't handle (this.getNumberOfCards() < $(this.id).readAttribute('data-card-count'))
  99 + // because the gaining column handles re-weighting for the losing column for AJAX efficiency.
  100 +
  101 + list.writeAttribute('data-card-count', this.getNumberOfCards());
  102 + },
  103 +
  104 +});
  105 +
  106 +var TaskBoardUtils = {
  107 +
  108 + serialize: function(list) {
  109 + var params = [];
  110 + list.childElements().each(function(card) {
  111 + params.push('sort[' + list.readAttribute('data-status-id') + '][]=' + card.readAttribute('data-issue-id'));
  112 + });
  113 + return params.join('&');
  114 + },
  115 +
  116 + moveParam: function(issue_id, new_status_id) {
  117 + return 'move[' + issue_id + ']=' + new_status_id;
  118 + },
  119 +
  120 + save: function(params) {
  121 + var options = Object.extend({
  122 + method: 'post',
  123 + parameters: params.join('&'),
  124 + onLoading: function() {
  125 + $('ajax-indicator').show();
  126 + },
  127 + onComplete: function() {
  128 + $('ajax-indicator').hide();
  129 + }
  130 + }, arguments[1] || {});
  131 +
  132 + new Ajax.Request(project_save_url, options);
  133 + },
  134 +
  135 + checkboxListener: function() {
  136 + TaskBoardUtils.hideButtonsIfNoneChecked();
  137 + $$('.card input[type="checkbox"]').invoke('observe', 'click', function(field) {
  138 + if (!$('taskboard-buttons').visible() && this.checked) {
  139 + $('taskboard-buttons').show();
  140 + }
  141 + if (!this.checked) {
  142 + TaskBoardUtils.hideButtonsIfNoneChecked();
  143 + }
  144 + });
  145 +
  146 + $('edit-issues').observe('click', function() {
  147 + location.href = '/issues/bulk_edit?' + TaskBoardUtils.serializeCheckedButtons();
  148 + });
  149 +
  150 + $('archive-issues').observe('click', function() {
  151 + new Ajax.Request(project_archive_url, {
  152 + method: 'post',
  153 + parameters: TaskBoardUtils.serializeCheckedButtons(),
  154 + onLoading: function() {
  155 + $('ajax-indicator').show();
  156 + },
  157 + onComplete: function() {
  158 + $('ajax-indicator').hide();
  159 + },
  160 + onSuccess: function() {
  161 + $$('.card input[type="checkbox"]').each(function(cb) {
  162 + if (cb.checked) {
  163 + $('issue_' + cb.value).remove();
  164 + }
  165 + });
  166 + }
  167 + });
  168 + });
  169 + },
  170 +
  171 + hideButtonsIfNoneChecked: function() {
  172 + var found_checked = false;
  173 + $$('.card input[type="checkbox"]').each(function(cb) {
  174 + if (cb.checked) {
  175 + found_checked = true;
  176 + throw $break;
  177 + }
  178 + });
  179 + if (!found_checked) {
  180 + $('taskboard-buttons').hide();
  181 + }
  182 + },
  183 +
  184 + serializeCheckedButtons: function() {
  185 + var params = [];
  186 + $$('.card input[type="checkbox"]').each(function(cb) {
  187 + if (cb.checked) {
  188 + params.push('ids[]=' + cb.value);
  189 + }
  190 + });
  191 + return params.join('&');
  192 + }
  193 +}
  194 +
  195 +var TaskBoardSettings = Class.create(TaskBoardSortable, {
  196 +
  197 + onChange: function() {
  198 + var weight = 0;
  199 + $(this.id).select(this.options.tag).each((function(el) {
  200 + var weightInput = el.down(this.options.weightSelector);
  201 + console.log(weightInput);
  202 + weightInput.writeAttribute('value', weight++);
  203 + }).bind(this));
  204 + }
  205 +
  206 +});
assets/stylesheets/taskboard.css View file @ 83f8099
  1 +.taskboard-wrapper {
  2 + width: 100%;
  3 + overflow: auto;
  4 +}
  5 +
  6 +.taskboard-pane {
  7 + float: left;
  8 + width: 250px;
  9 + margin: 0 4px;
  10 +}
  11 +
  12 +.taskboard-pane h2, .taskboard-pane h3 {
  13 + border: none;
  14 +}
  15 +
  16 +.taskboard-pane h3.status {
  17 + color: #666;
  18 + font-size: 14px;
  19 + border-bottom: solid 1px #666;
  20 +}
  21 +
  22 +.taskboard-pane h2 {
  23 + font-size: 18px;
  24 + text-align: center;
  25 +}
  26 +
  27 +.taskboard-pane ul {
  28 + border: solid 3px #eee;
  29 + list-style-type: none;
  30 + margin: 0 0 10px 0;
  31 + padding: 6px 6px 0 6px;
  32 + min-height: 50px;
  33 +}
  34 +
  35 +.taskboard-pane ul.empty {
  36 + border: dashed 3px #eee;
  37 + background: #fff;
  38 +}
  39 +
  40 +.taskboard-pane ul li.card {
  41 + list-style-type: none;
  42 + margin: 0 0 6px 0;
  43 + padding: 0;
  44 +}
  45 +
  46 +.taskboard-pane ul li.card.over-limit div.issue {
  47 + color: #aaa;
  48 + background: #ddd;
  49 +}
  50 +
  51 +.taskboard-pane ul li.card.bug div.issue {
  52 + border: solid 1px #D8000C;
  53 + border-top-width: 3px;
  54 +}
  55 +
  56 +.taskboard-pane ul li.card.feature div.issue {
  57 + border: solid 1px #4F8A10;
  58 + border-top-width: 3px;
  59 +}
  60 +
  61 +.taskboard-pane ul li.card.support div.issue {
  62 + border: solid 1px #00529B;
  63 + border-top-width: 3px;
  64 +}
  65 +
  66 +.taskboard-pane ul li.card.over-limit div.issue h3 a {
  67 + color: #aaa;
  68 +}
  69 +
  70 +.taskboard-pane ul li.card div.issue {
  71 + padding: 3px;
  72 +}
  73 +
  74 +.taskboard-pane ul li.card div.issue:hover {
  75 + cursor: pointer;
  76 +}
  77 +
  78 +.taskboard-pane ul li.card div.issue p.meta {
  79 + font-size: 11px;
  80 + margin: 0;
  81 + padding: 0;
  82 +}
  83 +
  84 +.taskboard-pane ul li.card div.issue div.issue-heading {
  85 + overflow: auto;
  86 + margin-bottom: 3px;
  87 +}
  88 +
  89 +.taskboard-pane ul li.card div.issue div.issue-heading p.issue-number {
  90 + float: left;
  91 +}
  92 +
  93 +.taskboard-pane ul li.card div.issue h3 {
  94 + font-size: 14px;
  95 + margin-bottom: 3px;
  96 +}
  97 +
  98 +.taskboard-pane ul li.card div.issue h3 a {
  99 + color: #000;
  100 +}
  101 +
  102 +.taskboard-pane ul li.card div.issue h3 a:hover {
  103 + color: #000;
  104 + text-decoration: none;
  105 + border-bottom: none;
  106 +}
  107 +
  108 +#main.nosidebar #content {
  109 + position: relative;
  110 +}
  111 +
  112 +#taskboard-buttons {
  113 + position: absolute;
  114 + top: 0;
  115 + right: 0;
  116 + background: #ccc;
  117 + padding: 3px;
  118 +}
config/locales/en.yml View file @ 83f8099
  1 +# English strings go here for Rails i18n
  2 +en:
  3 + label_task_board: Task Board
  4 + task_board_help: You must create columns for your task board. Each column can represent multiple issue statuses.
  5 + task_board_column_title: Column Title
  6 + task_board_create_column: Create Column
  7 + label_task_board_application_error: Unexpected application error
  8 + task_board_statuses: Statuses
  9 + task_board_columns: Columns
  10 + task_board_not_shown: Not Shown
  11 + task_board_visible_columns: Visible Columns
  12 + task_board_task_limit: Max Tasks
  13 + task_board_save_changes: Save Changes
  14 + task_board_issue_archived: This issue is archived from the task board view and will not display.
  15 + task_board_issue_unarchive: Unarchive Issue
  16 + task_board_issue_bulk_archive: Archive Selected Issues
  17 + task_board_issue_bulk_edit: Edit Selected Issues
config/routes.rb View file @ 83f8099
  1 +# Plugin's routes
  2 +# See: http://guides.rubyonrails.org/routing.html
  3 +
  4 +get 'projects/:project_id/taskboard', :to => 'taskboard#index'
  5 +post 'projects/:project_id/taskboard/save', :to => 'taskboard#save'
  6 +post 'projects/:project_id/taskboard/archive-issues', :to => 'taskboard#archive_issues'
  7 +post 'issues/:issue_id/taskboard-unarchive', :to => 'taskboard#unarchive_issue'
  8 +post 'projects/:project_id/taskboard/columns/create', :to => 'taskboard#create_column', :as => :project_taskboard_columns_create
  9 +delete 'projects/:project_id/taskboard/columns/:column_id/delete', :to => 'taskboard#delete_column', :as => :project_taskboard_columns_delete
  10 +put 'projects/:project_id/taskboard/columns/update', :to => 'taskboard#update_columns', :as => :project_taskboard_columns_update
db/migrate/001_create_task_board_issues.rb View file @ 83f8099
  1 +class CreateTaskBoardIssues < ActiveRecord::Migration
  2 + def change
  3 + create_table :task_board_issues do |t|
  4 + t.references :issue
  5 + t.integer :project_weight, :default => 0
  6 + t.integer :global_weight, :default => 0
  7 + t.integer :assignee_weight, :default => 0
  8 + t.boolean :is_archived, :default => false
  9 + end
  10 + end
  11 +end
db/migrate/002_create_task_board_columns.rb View file @ 83f8099
  1 +class CreateTaskBoardColumns < ActiveRecord::Migration
  2 + def change
  3 + create_table :task_board_columns do |t|
  4 + t.references :project
  5 + t.string :title
  6 + t.integer :weight, :default => 0
  7 + t.integer :max_issues, :default => 0
  8 + end
  9 +
  10 + create_table :issue_statuses_task_board_columns, :id => false do |t|
  11 + t.references :issue_status, :task_board_column
  12 + end
  13 +
  14 + add_index :issue_statuses_task_board_columns, [:issue_status_id, :task_board_column_id], {:name => 'issue_statuses_task_board_columns_idx'}
  15 + end
  16 +end
  1 +require 'redmine'
  2 +require 'redmine_task_board_hook_listener'
  3 +
  4 +Rails.configuration.to_prepare do
  5 + require_dependency 'projects_helper'
  6 + ProjectsHelper.send(:include, RedmineTaskBoardSettingsPatch) unless ProjectsHelper.included_modules.include?(RedmineTaskBoardSettingsPatch)
  7 +end
  8 +
  9 +Redmine::Plugin.register :redmine_task_board do
  10 + name 'Redmine Task Board'
  11 + author 'Austin Smith'
  12 + description 'Add a Kanban-style task board tab to projects'
  13 + version '0.0.1'
  14 + url 'https://github.com/netaustin/redmine_task_board'
  15 + author_url 'http://www.alleyinteractive.com/'
  16 +
  17 + project_module :taskboard do
  18 + permission :edit_taskboard, {:projects => :settings, :taskboard => [:create_column, :delete_column, :update_columns]}, :require => :member
  19 + permission :view_taskboard, {:taskboard => [:index, :save, :archive_issues, :unarchive_issue]}, :require => :member
  20 + end
  21 + menu :project_menu, :taskboard, { :controller => 'taskboard', :action => 'index' }, :caption => 'Task Board', :before => :issues, :param => :project_id
  22 +end
lib/redmine_task_board_hook_listener.rb View file @ 83f8099
  1 +class RedmineTaskBoardHookListener < Redmine::Hook::ViewListener
  2 + render_on :view_issues_show_description_bottom, :partial => "taskboard/issue_description"
  3 +end
lib/redmine_task_board_settings_patch.rb View file @ 83f8099
  1 +require_dependency 'projects_helper'
  2 +
  3 +module RedmineTaskBoardSettingsPatch
  4 + def self.included(base) # :nodoc:
  5 + base.send(:include, InstanceMethods)
  6 +
  7 + base.class_eval do
  8 + alias_method_chain :project_settings_tabs, :taskboard_tab
  9 + end
  10 + end
  11 +
  12 + module InstanceMethods
  13 + # Adds a task board tab to the user administration page
  14 + def project_settings_tabs_with_taskboard_tab
  15 + tabs = project_settings_tabs_without_taskboard_tab
  16 + if @project.allows_to?({ :controller => "taskboard", :action => "index" }) then
  17 + tabs << { :name => 'taskboard', :partial => 'settings/project', :label => :label_task_board}
  18 + end
  19 + return tabs
  20 + end
  21 + end
  22 +end
test/functional/taskboard_controller_test.rb View file @ 83f8099
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class TaskboardControllerTest < ActionController::TestCase
  4 + # Replace this with your real tests.
  5 + def test_truth
  6 + assert true
  7 + end
  8 +end
test/test_helper.rb View file @ 83f8099
  1 +# Load the normal Rails helper
  2 +require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper')
  3 +
  4 +# Ensure that we are using the temporary fixture path
  5 +Engines::Testing.set_fixture_path
test/unit/task_board_column_test.rb View file @ 83f8099
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class TaskBoardColumnTest < ActiveSupport::TestCase
  4 +
  5 + # Replace this with your real tests.
  6 + def test_truth
  7 + assert true
  8 + end
  9 +end
test/unit/task_board_issue_test.rb View file @ 83f8099
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class TaskBoardIssueTest < ActiveSupport::TestCase
  4 +
  5 + # Replace this with your real tests.
  6 + def test_truth
  7 + assert true
  8 + end
  9 +end