├── .github └── FUNDING.yml ├── app ├── helpers │ ├── issue_helper.rb │ ├── journal_helper.rb │ └── kanban_helper.rb ├── views │ ├── issue │ │ └── index.html.erb │ ├── journal │ │ └── index.html.erb │ ├── kanban │ │ ├── _small_card.html.erb │ │ ├── _normal_card_estimated_hours.html.erb │ │ ├── _normal_card_days_left.html.erb │ │ ├── _normal_card_spent_hours.html.erb │ │ └── index.html.erb │ └── my │ │ └── blocks │ │ └── _kanban_widget.html.erb └── controllers │ ├── issue_controller.rb │ ├── journal_controller.rb │ └── kanban_controller.rb ├── assets ├── images │ ├── filters_ss.png │ ├── show_pearent_ss.png │ ├── roles_management.png │ ├── kanban_board_overview.png │ └── kanban_board_small_card.png ├── stylesheets │ └── kanban.css └── javascripts │ ├── kanban.js │ ├── hotkeys.js │ └── jquery.floatThead.js ├── test ├── test_helper.rb └── functional │ ├── issue_controller_test.rb │ ├── kanban_controller_test.rb │ └── journal_controller_test.rb ├── config ├── routes.rb └── locales │ ├── ja.yml │ ├── en.yml │ ├── de.yml │ ├── pt-BR.yml │ ├── it.yml │ ├── fr.yml │ ├── pl.yml │ ├── es.yml │ └── ru.yml ├── init.rb ├── LICENSE ├── lib └── kanban │ └── constants.rb └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: happy-se-life 2 | -------------------------------------------------------------------------------- /app/helpers/issue_helper.rb: -------------------------------------------------------------------------------- 1 | module IssueHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/journal_helper.rb: -------------------------------------------------------------------------------- 1 | module JournalHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/issue/index.html.erb: -------------------------------------------------------------------------------- 1 |
23 |
24 | ### Ticket filters
25 |
26 |
27 | ### Small card view
28 | You can display more cards than normal size cards at once.
29 |
30 |
31 | ### Show parent task in issues cards
32 | It is possible to show\hide the issue parent from the filter section.
33 |
34 |
35 | ## Keyboard Shortcuts
36 | - o : Show login user only.
37 | - e : Show everyone in the project.
38 | - d+u : Due date set to unspecified.
39 | - d+o : Due date set to overdue.
40 | - d+t : Due date set to today.
41 | - d+w : Due date set to this week.
42 | - n : Show by normal size card with days left.
43 | - k : Show by normal size card with estimated hours.
44 | - p : Show by normal size card with spent hours.
45 | - s : Show by small size card.
46 | - l : Select all open issues.
47 | - h : Show this help.
48 |
49 | ## Required Redmine version
50 | * 3.4.6.stable ~ 6.0.2.stable
51 |
52 | ## Install
53 |
54 | 1. Move to `plugins` folder.
55 | 56 | git clone https://github.com/happy-se-life/kanban.git 57 |58 | 59 | 2. Edit `lib/kanban/constants.rb` for your environment. 60 | 61 | 3. Restart redmine. 62 | 63 | 4. Enable role permission to each users groups 64 | 65 |
66 |
67 | 5. Enable modules for each project.
68 |
69 | ## Uninstall
70 |
71 | 1. Move to plugins folder.
72 |
73 | 2. Remove plugins folder.
74 | 75 | rm -rf kanban 76 |77 | 78 | 3. Restart redmine. 79 | 80 | ## Notes 81 | * It has only been used by small organizations upto 30 members before. 82 | * Therefore, the visibility authority is loosely implemented. 83 | * However, I think it is a trade-off with ease of use. 84 | 85 | ## License 86 | * MIT Lisense 87 | 88 | ## Thanks for the great contributors who helped with the localization 89 | * @aivorra for Spanish 90 | * @camlafit for French 91 | * @karnow98 for Polish 92 | * @deryaba for Russian 93 | * @HMT-HRO-MaMe for German 94 | * @guilhermelinhares for European Portuguese 95 | * @Wrightie for Italian 96 | 97 | ## Thanks for the great library 98 | * [mkoryak/floatThead](https://github.com/mkoryak/floatThead) 99 | * [jaywcjlove/hotkeys](https://github.com/jaywcjlove/hotkeys) 100 | 101 | ## Contact me 102 | * If you have any questions or ideas for improvement, please register with issue. 103 | -------------------------------------------------------------------------------- /assets/stylesheets/kanban.css: -------------------------------------------------------------------------------- 1 | /* Normal priority issue */ 2 | .my-issue-card { 3 | padding: 5px; 4 | border: solid 1px #d5d5d5; 5 | background-color: #ffffdd; 6 | margin: 8px 4px 8px 4px; 7 | word-wrap: break-word; 8 | text-align: left; 9 | white-space: normal; 10 | cursor: pointer; 11 | position: relative; 12 | border-radius:3px; 13 | } 14 | 15 | /* High priority issue */ 16 | .my-issue-card-high-priority { 17 | padding: 5px; 18 | border: solid 1px #d5d5d5; 19 | /* background-color: #ffafaf; */ 20 | background-color: #FFD4D4; 21 | margin: 8px 4px 8px 4px; 22 | word-wrap: break-word; 23 | text-align: left; 24 | white-space: normal; 25 | cursor: pointer; 26 | position: relative; 27 | border-radius:3px; 28 | } 29 | 30 | /* Low priority issue */ 31 | .my-issue-card-low-priority { 32 | padding: 5px; 33 | border: solid 1px #d5d5d5; 34 | background-color: #EAF7FF; 35 | margin: 8px 4px 8px 4px; 36 | word-wrap: break-word; 37 | text-align: left; 38 | white-space: normal; 39 | cursor: pointer; 40 | position: relative; 41 | border-radius:3px; 42 | } 43 | 44 | /* Tilt the card wheb dragged */ 45 | .dragged-issue-card { 46 | transform: rotate(-10deg); 47 | } 48 | 49 | /* Note on sidebar */ 50 | .my-journal-table { 51 | width: 100%; 52 | border: solid 1px #d5d5d5; 53 | background-color: #ffffff; 54 | margin: 8px 0px 8px 0px; 55 | word-break : break-all; 56 | text-align: left; 57 | white-space: normal; 58 | border-collapse: separate; 59 | border-radius:4px; 60 | } 61 | .my-journal-table td { 62 | text-align: left !important; 63 | } 64 | 65 | /* New comment table */ 66 | .my-comment-table { 67 | width: 100%; 68 | margin: 8px 0px 8px 0px; 69 | } 70 | 71 | /* New comment textarea */ 72 | .my-comment-textarea { 73 | box-sizing:border-box; 74 | resize: none; 75 | width: 100%; 76 | } 77 | 78 | /* String ellipsis */ 79 | .my-string-ellipsis { 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | white-space: nowrap; 83 | } 84 | 85 | /* Normal priority issue (small size) */ 86 | .my-issue-card-small { 87 | padding: 3px; 88 | border: solid 1px #d5d5d5; 89 | background-color: #ffffdd; 90 | margin: 4px 4px 4px 4px; 91 | word-wrap: break-word; 92 | text-align: left; 93 | white-space: normal; 94 | cursor: pointer; 95 | position: relative; 96 | border-radius:3px; 97 | } 98 | /* High priority issue (small size) */ 99 | .my-issue-card-high-priority-small { 100 | padding: 3px; 101 | border: solid 1px #d5d5d5; 102 | /* background-color: #ffafaf; */ 103 | background-color: #FFD4D4; 104 | margin: 4px 4px 4px 4px; 105 | word-wrap: break-word; 106 | text-align: left; 107 | white-space: normal; 108 | cursor: pointer; 109 | position: relative; 110 | border-radius:3px; 111 | } 112 | 113 | /* Low priority issue (small size) */ 114 | .my-issue-card-low-priority-small { 115 | padding: 3px; 116 | border: solid 1px #d5d5d5; 117 | background-color: #EAF7FF; 118 | margin: 4px 4px 4px 4px; 119 | word-wrap: break-word; 120 | text-align: left; 121 | white-space: normal; 122 | cursor: pointer; 123 | position: relative; 124 | border-radius:3px; 125 | } 126 | 127 | .ancestor{ 128 | color:rgb(119, 151, 194); 129 | font-size: 10px; 130 | } 131 | .context-menu-selection p { 132 | color:rgb(184, 224, 244); 133 | } 134 | 135 | #kanban_table { 136 | width: 100%; 137 | table-layout: auto; 138 | } 139 | 140 | .floatThead-container { 141 | width: 100% !important; 142 | } 143 | -------------------------------------------------------------------------------- /app/controllers/journal_controller.rb: -------------------------------------------------------------------------------- 1 | class JournalController < ApplicationController 2 | 3 | def index 4 | end 5 | 6 | # 7 | # Get journals to display on sidebar 8 | # 9 | def get_journal 10 | # Get POST value 11 | card_id = params[:card_id] 12 | 13 | # Target issue ID 14 | issue_array = card_id.split("-") 15 | issue_id = issue_array[1].to_i 16 | 17 | # Declaring variable to return 18 | result_hash = {} 19 | 20 | # Target issue 21 | issue = Issue.find(issue_id); 22 | 23 | # ヘッダ表示 24 | notes_string ="
#" + issue_id.to_s + "
" 25 | notes_string ="#" + issue_id.to_s + "
" 26 | 27 | # Building html of issue description 28 | if !issue.description.blank? then 29 | user = User.find(issue.author_id) 30 | notes_string +="" + I18n.t(:field_description) + "
" 31 | notes_string += "| " 34 | notes_string += "" 35 | notes_string += issue.created_on.strftime("%Y-%m-%d %H:%M:%S") 36 | notes_string += "" 37 | notes_string += " " 38 | notes_string += user.name 39 | notes_string += " | " 40 | notes_string += "
|---|
| " 43 | notes_string += CGI.escapeHTML(trim_notes(issue.description)) 44 | notes_string += " | " 45 | notes_string += "
" + I18n.t(:kanban_label_recent_history) + "
" 61 | end 62 | 63 | # Building html of notes 64 | if Kanban::Constants::ORDER_NOTES == "ASC" then 65 | notes.reverse_each {|note| 66 | notes_string += note_to_html(issue_id, note) 67 | } 68 | else 69 | # DESC 70 | notes.each {|note| 71 | notes_string += note_to_html(issue_id, note) 72 | } 73 | end 74 | 75 | # Buiding input area for new note 76 | notes_string += "" + I18n.t(:kanban_label_add_notes) + "
" 77 | notes_string += "| " 78 | notes_string += "" 79 | notes_string += "" 80 | notes_string += " |
| " 136 | html += "" 137 | html += note.created_on.strftime("%Y-%m-%d %H:%M:%S") 138 | html += "" 139 | html += " " 140 | html += user.name 141 | html += " | " 142 | html += "
|---|
| " 145 | html += CGI.escapeHTML(trim_notes(note.notes)) 146 | html += " | " 147 | html += "
| <%= I18n.t(:field_project) %> | 106 | <% @status_fields_array.each do |status_id| %> 107 |<%= @issue_statuses_hash[status_id] %> | 108 | <% end %> 109 |
|---|---|
| <%= link_to project.name, project_path(project), class: 'project-link' %> | 115 | <% @status_fields_array.each do |status_id| %> 116 |
117 | <%
118 | # Get issues for this project and status
119 | issues_for_status = @issues_hash[status_id] || []
120 | project_issues = issues_for_status.select { |issue| issue.project_id == project.id }
121 | %>
122 | <% project_issues.each do |issue| %>
123 | <%
124 | # Determine CSS class based on priority
125 | priority_class = if issue.priority_id >= Kanban::Constants::DEFAULT_HIGH_PRIORITY_ISSUE_ID
126 | 'my-issue-card-high-priority'
127 | elsif issue.priority_id == Kanban::Constants::DEFAULT_NORMAL_PRIORITY_ISSUE_ID
128 | 'my-issue-card'
129 | else
130 | 'my-issue-card-low-priority'
131 | end
132 | %>
133 |
134 |
158 | <% end %>
159 |
135 | <%= issue.tracker.name %> #<%= issue.id %>
136 |
137 |
138 | <%= link_to issue.subject, issue_path(issue), class: 'issue-link' %>
139 |
140 | 141 |
142 |
143 | <% if issue.due_date %>
144 | Due: <%= issue.due_date.strftime('%m/%d') %>
145 | <% else %>
146 | Due: -
147 | <% end %>
148 |
149 |
150 | <% if issue.estimated_hours %>
151 | Est: <%= issue.estimated_hours %>h
152 | <% else %>
153 | Est: -
154 | <% end %>
155 |
156 |
157 | |
160 | <% end %>
161 |
| " + label_recent_history_is_here + " |
Not assigned
"); 247 | } 248 | } 249 | if (data.result == "NG") { 250 | alert("Operation not permitted") 251 | // Reload page 252 | $('#form1').submit(); 253 | } 254 | }) 255 | // Case ajax failed 256 | .fail( (data) => { 257 | console.log("AJAX FAILED."); 258 | // Reload page 259 | $('#form1').submit(); 260 | }); 261 | } 262 | 263 | // 264 | // Get journal 265 | // 266 | function getJournal(card_id) { 267 | // AJAX 268 | $.ajax({ 269 | url:'./get_journal', 270 | type:'POST', 271 | data:{ 272 | 'card_id' :card_id , 273 | }, 274 | dataType: 'json', 275 | async: true, 276 | global: false 277 | }) 278 | // Case ajax succeed 279 | .done( (data) => { 280 | console.log(data.result); 281 | if (data.result == "OK") { 282 | // Display on sidebar 283 | $('#sidebar').html(data.notes); 284 | // Register click event 285 | $('#submit-journal-button').on('click',function(){ 286 | putJournal(card_id); 287 | }); 288 | } 289 | }) 290 | // Case ajax failed 291 | .fail( (data) => { 292 | console.log("AJAX FAILED."); 293 | }) 294 | } 295 | 296 | // 297 | // Add new journal 298 | // 299 | function putJournal(card_id) { 300 | var note = $('#comment_area').val(); 301 | // AJAX 302 | $.ajax({ 303 | url:'./put_journal', 304 | type:'POST', 305 | data:{ 306 | 'card_id' :card_id , 307 | 'note' : note 308 | }, 309 | dataType: 'json', 310 | async: true, 311 | }) 312 | // Case ajax succeed 313 | .done( (data) => { 314 | console.log(data.result); 315 | if (data.result == "OK") { 316 | // Reread journal 317 | getJournal(card_id); 318 | } 319 | }) 320 | // Case ajax failed 321 | .fail( (data) => { 322 | console.log("AJAX FAILED."); 323 | }) 324 | } 325 | 326 | // Suppress messages "Leave this site?" 327 | Object.defineProperty(window, 'onbeforeunload', { 328 | set(newValue) { 329 | if (typeof newValue === 'function') window.onbeforeunload = null; 330 | } 331 | }); 332 | 333 | // Keyboard Shortcuts 334 | hotkeys('o,e,k,d+u,d+o,d+t,d+w,n,s,h,l,p', function(event,handler) { 335 | switch(handler.key){ 336 | // assignee == (login user) 337 | case "o": 338 | $('#user_id').val(login_user_id); 339 | break; 340 | // assignee == (everyone in the project) 341 | case "e": 342 | $('#user_id').val("unspecified"); 343 | break; 344 | // due_date == unspecified 345 | case "d+u": 346 | $('#due_date').val("unspecified"); 347 | break; 348 | // due_date == overdue 349 | case "d+o": 350 | $('#due_date').val("overdue"); 351 | break; 352 | // due_date == today 353 | case "d+t": 354 | $('#due_date').val("today"); 355 | break; 356 | // due_date == thisweek 357 | case "d+w": 358 | $('#due_date').val("thisweek"); 359 | break; 360 | // card_size == normal_days_left 361 | case "n": 362 | $('#card_size').val("normal_days_left"); 363 | break; 364 | // card_size == normal_estimated_hours 365 | case "k": 366 | $('#card_size').val("normal_estimated_hours"); 367 | break; 368 | // card_size == normal_spent_hours 369 | case "p": 370 | $('#card_size').val("normal_spent_hours"); 371 | break; 372 | // card_size == small 373 | case "s": 374 | $('#card_size').val("small"); 375 | break; 376 | // show this 377 | case "h": 378 | $('#keyboard-chortcut-dialog').dialog('open'); 379 | return; 380 | // select all open issues 381 | case "l": 382 | var tmp = $('#open-field-ids').html().split(' '); 383 | for(let i in tmp) { 384 | $("[id^=field-" + tmp[i] + "-]").find('input').click(); 385 | } 386 | return; 387 | } 388 | // reload page 389 | $('#form1').submit(); 390 | }); 391 | -------------------------------------------------------------------------------- /assets/javascripts/hotkeys.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * hotkeys-js v3.8.1 3 | * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. 4 | * 5 | * Copyright (c) 2020 kenny wong