6 Commits
v1.0 ... master

Author SHA1 Message Date
60dfa7ae4a Added a setting to apply green/red colors to Start/Stop 2026-03-21 07:53:56 -04:00
490fbd476d Change link to be green to start and red to stop 2026-03-19 22:33:53 -04:00
Jens Kraemer
527f72f93b modern redmine + ruby versions 2025-12-04 10:20:10 +08:00
Jens Kraemer
8e34eb627c fixes various UI issues 2025-06-24 15:48:03 +08:00
Jens Kraemer
3c709b1329 v1.1 for Redmine 6 2025-06-24 15:26:21 +08:00
Jens Kraemer
776c0cd823 switch to svg icons 2025-06-07 11:41:49 +08:00
16 changed files with 75 additions and 46 deletions

View File

@@ -14,11 +14,10 @@ jobs:
strategy: strategy:
matrix: matrix:
redmine: redmine:
- '5.0' - '6.0'
- '5.1' - '6.1'
ruby: ruby:
- '3.0' - '3.3'
- '3.1'
database: database:
- postgresql - postgresql
- mysql - mysql

View File

@@ -2,3 +2,8 @@
<label for="settings_default_activity"><%= t '.label_default_activity' %> </label> <label for="settings_default_activity"><%= t '.label_default_activity' %> </label>
<%= select_tag 'settings[default_activity]', options_from_collection_for_select( [['always_ask', t('.label_always_ask')], ['system', t('.label_system')]] + TimeEntryActivity.system.active.to_a.pluck(:id, :name), :first, :last, Stopwatch.settings['default_activity'] ) %> <%= select_tag 'settings[default_activity]', options_from_collection_for_select( [['always_ask', t('.label_always_ask')], ['system', t('.label_system')]] + TimeEntryActivity.system.active.to_a.pluck(:id, :name), :first, :last, Stopwatch.settings['default_activity'] ) %>
</p> </p>
<p>
<label for="settings_highlighted_links"><%= t '.label_highlighted_links' %> </label>
<%= check_box_tag 'settings[highlighted_links]', 1, Setting.plugin_stopwatch[:highlighted_links] %>
</p>

View File

@@ -1,2 +1,3 @@
<%= javascript_include_tag "stopwatch", plugin: "stopwatch" %> <%= javascript_include_tag "stopwatch", plugin: "stopwatch" %>
<%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %> <%= stylesheet_link_tag "stopwatch", plugin: "stopwatch" %>
<%= stylesheet_link_tag "highlighted_links", plugin: "stopwatch" if Stopwatch.highlighted_links %>

View File

@@ -2,9 +2,9 @@
<% time_entry = @time_entries.first %> <% time_entry = @time_entries.first %>
<li> <li>
<% if User.current.is_running_timer? time_entry %> <% if User.current.is_running_timer? time_entry %>
<%= context_menu_link l(:label_stopwatch_stop), stop_stopwatch_timer_path(time_entry, context: '1'), class: 'icon icon-time', remote: true, method: :put %> <%= context_menu_link sprite_icon(:time, l(:label_stopwatch_stop)), stop_stopwatch_timer_path(time_entry, context: '1'), class: 'icon', remote: true, method: :put %>
<% else %> <% else %>
<%= context_menu_link l(:label_stopwatch_start), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon icon-time', remote: true, method: :put %> <%= context_menu_link sprite_icon(:time, l(:label_stopwatch_start)), start_stopwatch_timer_path(time_entry, context: '1'), class: 'icon', remote: true, method: :put %>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>

View File

@@ -1,6 +1,6 @@
contextMenuHide(); contextMenuHide();
window.stopwatch.updateStartStopLink( window.stopwatch.updateStartStopLink(
'#stopwatch_stop_timer_<%= @issue.id %>', '#stopwatch_stop_timer_<%= @issue.id %>',
'<%= j Stopwatch::IssueLinks.new(@issue).start_timer %>' '<%= j Stopwatch::IssueLinks.new(@issue, self).start_timer %>'
); );
window.stopwatch.timerStopped(); window.stopwatch.timerStopped();

View File

@@ -17,11 +17,11 @@
<td class="hours"><%= html_hours(format_hours(entry.hours)) %></td> <td class="hours"><%= html_hours(format_hours(entry.hours)) %></td>
<td class="buttons"> <td class="buttons">
<% if running %> <% if running %>
<%= link_to t(:label_stopwatch_stop), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %> <%= link_to sprite_icon(:time, t(:label_stopwatch_stop)), stop_stopwatch_timer_path(entry), remote: true, class: "icon-only", method: :put %>
<% else %> <% else %>
<%= link_to t(:label_stopwatch_start), start_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-time", method: :put %> <%= link_to sprite_icon(:time, t(:label_stopwatch_start)), start_stopwatch_timer_path(entry), remote: true, class: "icon-only", method: :put %>
<% end %> <% end %>
<%= link_to l(:button_edit), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only icon-edit" if entry.editable_by? User.current %> <%= link_to sprite_icon(:edit, l(:button_edit)), edit_stopwatch_timer_path(entry), remote: true, class: "icon-only" if entry.editable_by? User.current %>
</td> </td>
</tr> </tr>
<% end -%> <% end -%>

View File

@@ -74,7 +74,7 @@ window.initStopwatch = function(config){
$('a.stopwatch_issue_timer').each(function(){ $('a.stopwatch_issue_timer').each(function(){
var a = $(this); var a = $(this);
a.attr('href', a.attr('href').replace(/stop$/, 'start')); a.attr('href', a.attr('href').replace(/stop$/, 'start'));
a.text(locales.startTimer); a.find('span').text(locales.startTimer);
}); });
}, },
timerStarted: function(data){ timerStarted: function(data){
@@ -94,10 +94,10 @@ window.initStopwatch = function(config){
if(data.issue_id) { if(data.issue_id) {
if(a.data('issueId') == data.issue_id) { if(a.data('issueId') == data.issue_id) {
a.attr('href', href.replace(/start$/, 'stop')); a.attr('href', href.replace(/start$/, 'stop'));
a.text(locales.stopTimer); a.find('span').text(locales.stopTimer);
} else { } else {
a.attr('href', href.replace(/stop$/, 'start')); a.attr('href', href.replace(/stop$/, 'start'));
a.text(locales.startTimer); a.find('span').text(locales.startTimer);
} }
} }
}); });

View File

@@ -0,0 +1,22 @@
a.stopwatch_issue_timer[href*="stop"] {
color: red;
}
a.stopwatch_issue_timer[href*="start"] {
color: limegreen;
}
/* Mobile only */
@media (max-width: 899px) {
a.stopwatch_issue_timer[href*="stop"] {
color: red;
background-color: #ffe5e5;
border: 1px solid red;
}
a.stopwatch_issue_timer[href*="start"] {
color: limegreen;
background-color: #eaffea;
border: 1px solid limegreen;
}
}

View File

@@ -5,6 +5,7 @@ en:
settings: settings:
label_always_ask: 'Always ask' label_always_ask: 'Always ask'
label_default_activity: 'Default activity for "Start tracking"' label_default_activity: 'Default activity for "Start tracking"'
label_highlighted_links: "Color-code Start/Stop"
label_system: 'Use system default' label_system: 'Use system default'
stopwatch_timers: stopwatch_timers:
entries_list: entries_list:

View File

@@ -3,9 +3,9 @@ Redmine::Plugin.register :stopwatch do
author 'Jens Krämer' author 'Jens Krämer'
author_url 'https://jkraemer.net/' author_url 'https://jkraemer.net/'
description "Start/stop timer and quick access to today's time bookings for Redmine" description "Start/stop timer and quick access to today's time bookings for Redmine"
version '1.0.0' version '1.1.0'
requires_redmine version_or_higher: '5.0.0' requires_redmine version_or_higher: '6.0.0'
settings default: { settings default: {
'default_activity' => 'always_ask', 'default_activity' => 'always_ask',
}, partial: 'stopwatch/settings' }, partial: 'stopwatch/settings'

View File

@@ -9,7 +9,6 @@ module Stopwatch
Stopwatch::Hooks # just load it Stopwatch::Hooks # just load it
end end
def self.settings def self.settings
Setting.plugin_stopwatch Setting.plugin_stopwatch
end end
@@ -42,4 +41,8 @@ module Stopwatch
return activities.detect{ |a| a == default || a.parent == default } return activities.detect{ |a| a == default || a.parent == default }
end end
end end
def self.highlighted_links
settings[:highlighted_links]
end
end end

View File

@@ -10,9 +10,9 @@ module Stopwatch
User.current.allowed_to?(:log_time, issue.project) User.current.allowed_to?(:log_time, issue.project)
t = Stopwatch::IssueTimer.new(issue: issue) t = Stopwatch::IssueTimer.new(issue: issue)
if t.running? if t.running?
link << IssueLinks.new(issue).stop_timer link << IssueLinks.new(issue, self).stop_timer
else else
link << IssueLinks.new(issue).start_timer link << IssueLinks.new(issue, self).start_timer
end end
end end
super + content_tag(:li, link.html_safe) super + content_tag(:li, link.html_safe)
@@ -26,4 +26,3 @@ module Stopwatch
end end
end end
end end

View File

@@ -1,26 +1,25 @@
module Stopwatch module Stopwatch
class IssueLinks < Struct.new(:issue) class IssueLinks < Struct.new(:issue, :context)
include ActionView::Helpers::UrlHelper
include Rails.application.routes.url_helpers
def start_timer def start_timer
link_to(I18n.t(:label_stopwatch_start), context.link_to(
start_issue_timer_path(issue), context.sprite_icon(:time, I18n.t(:label_stopwatch_start)),
class: 'icon icon-time stopwatch_issue_timer', context.start_issue_timer_path(issue),
class: 'icon stopwatch_issue_timer',
data: { issue_id: issue.id }, data: { issue_id: issue.id },
remote: true, remote: true,
method: 'post') method: 'post'
)
end end
def stop_timer def stop_timer
link_to(I18n.t(:label_stopwatch_stop), context.link_to(
stop_issue_timer_path(issue), context.sprite_icon(:time, I18n.t(:label_stopwatch_stop)),
class: 'icon icon-time stopwatch_issue_timer', context.stop_issue_timer_path(issue),
class: 'icon stopwatch_issue_timer',
data: { issue_id: issue.id }, data: { issue_id: issue.id },
remote: true, remote: true,
method: 'post') method: 'post'
)
end end
# to make route helpers happy # to make route helpers happy

View File

@@ -8,9 +8,9 @@ module Stopwatch
if User.current.allowed_to?(:log_time, issue.project) if User.current.allowed_to?(:log_time, issue.project)
t = Stopwatch::IssueTimer.new(issue: issue) t = Stopwatch::IssueTimer.new(issue: issue)
if t.running? if t.running?
link << IssueLinks.new(issue).stop_timer link << IssueLinks.new(issue, self).stop_timer
else else
link << IssueLinks.new(issue).start_timer link << IssueLinks.new(issue, self).start_timer
end end
end end
link.html_safe + super link.html_safe + super

View File

@@ -1,4 +1,4 @@
require File.expand_path('../../test_helper', __FILE__) require File.expand_path('../test_helper', __dir__)
class TicketTimerTest < Redmine::IntegrationTest class TicketTimerTest < Redmine::IntegrationTest
include ActiveJob::TestHelper include ActiveJob::TestHelper
@@ -30,6 +30,7 @@ class TicketTimerTest < Redmine::IntegrationTest
assert_not_running assert_not_running
get "/issues/1" get "/issues/1"
assert_response :success
assert_select "div.contextual a", text: /start tracking/i assert_select "div.contextual a", text: /start tracking/i
assert_no_difference ->{TimeEntry.count} do assert_no_difference ->{TimeEntry.count} do
post "/issues/1/timer/start", xhr: true post "/issues/1/timer/start", xhr: true
@@ -77,7 +78,7 @@ class TicketTimerTest < Redmine::IntegrationTest
TimeEntry.delete_all TimeEntry.delete_all
assert_no_difference ->{TimeEntry.count} do assert_no_difference ->{TimeEntry.count} do
post "/issues/1/timer/start", xhr: true post "/issues/1/timer/start", xhr: true
assert_response 200 assert_response :ok
end end
end end
@@ -86,7 +87,7 @@ class TicketTimerTest < Redmine::IntegrationTest
TimeEntry.delete_all TimeEntry.delete_all
with_settings plugin_stopwatch: { 'default_activity' => 'system'} do with_settings plugin_stopwatch: { 'default_activity' => 'system'} do
post "/issues/1/timer/start", xhr: true post "/issues/1/timer/start", xhr: true
assert_response 201 assert_response :created
end end
assert te = TimeEntry.last assert te = TimeEntry.last
assert_equal 1, te.issue_id assert_equal 1, te.issue_id
@@ -98,18 +99,17 @@ class TicketTimerTest < Redmine::IntegrationTest
TimeEntry.delete_all TimeEntry.delete_all
with_settings plugin_stopwatch: { 'default_activity' => '9'} do with_settings plugin_stopwatch: { 'default_activity' => '9'} do
post "/issues/1/timer/start", xhr: true post "/issues/1/timer/start", xhr: true
assert_response 201 assert_response :created
end end
assert te = TimeEntry.last assert te = TimeEntry.last
assert_equal 1, te.issue_id assert_equal 1, te.issue_id
assert_equal 9, te.activity_id assert_equal 9, te.activity_id
end end
private private
def assert_not_running def assert_not_running
refute Stopwatch::Timer.new(User.find(@user.id)).running? assert_not Stopwatch::Timer.new(User.find(@user.id)).running?
end end
def assert_running def assert_running