Skip to content

Commit b5570d3

Browse files
authored
Display current stopwatch in navbar (#14122)
* add notification about running stopwatch to header * serialize seconds, duration in stopwatches api * ajax update stopwatch i should get my testenv working locally... * new variant: hover dialog * noscript compatibility * js: live-update stopwatch time * js live update robustness
1 parent 56a8929 commit b5570d3

File tree

15 files changed

+226
-15
lines changed

15 files changed

+226
-15
lines changed

‎integrations/api_issue_stopwatch_test.go‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package integrations
77
import (
88
"net/http"
99
"testing"
10-
"time"
1110

1211
"code.gitea.io/gitea/models"
1312
api "code.gitea.io/gitea/modules/structs"
@@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) {
3130
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
3231
if assert.Len(t, apiWatches, 1) {
3332
assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
34-
apiWatches[0].Created = time.Time{}
35-
assert.EqualValues(t, api.StopWatch{
36-
Created: time.Time{},
37-
IssueIndex: issue.Index,
38-
IssueTitle: issue.Title,
39-
RepoName: repo.Name,
40-
RepoOwnerName: repo.OwnerName,
41-
}, *apiWatches[0])
33+
assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
34+
assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
35+
assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
36+
assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
37+
assert.Greater(t, int64(apiWatches[0].Seconds), int64(0))
4238
}
4339
}
4440

‎integrations/attachment_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) {
7272
resp := session.MakeRequest(t, req, http.StatusOK)
7373
htmlDoc := NewHTMLParser(t, resp.Body)
7474

75-
link, exists := htmlDoc.doc.Find("form").Attr("action")
75+
link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action")
7676
assert.True(t, exists, "The template has changed")
7777

7878
postData := map[string]string{

‎models/issue_stopwatch.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ type Stopwatch struct {
1919
CreatedUnix timeutil.TimeStamp `xorm:"created"`
2020
}
2121

22+
// Seconds returns the amount of time passed since creation, based on local server time
23+
func (s Stopwatch) Seconds() int64 {
24+
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
25+
}
26+
27+
// Duration returns a human-readable duration string based on local server time
28+
func (s Stopwatch) Duration() string {
29+
return SecToTime(s.Seconds())
30+
}
31+
2232
func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
2333
sw = new(Stopwatch)
2434
exists, err = e.

‎modules/convert/issue.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) {
147147

148148
result = append(result, api.StopWatch{
149149
Created: sw.CreatedUnix.AsTime(),
150+
Seconds: sw.Seconds(),
151+
Duration: sw.Duration(),
150152
IssueIndex: issue.Index,
151153
IssueTitle: issue.Title,
152154
RepoOwnerName: repo.OwnerName,

‎modules/structs/issue_stopwatch.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
type StopWatch struct {
1313
// swagger:strfmt date-time
1414
Created time.Time `json:"created"`
15+
Seconds int64 `json:"seconds"`
16+
Duration string `json:"duration"`
1517
IssueIndex int64 `json:"issue_index"`
1618
IssueTitle string `json:"issue_title"`
1719
RepoOwnerName string `json:"repo_owner_name"`

‎options/locale/locale_en-US.ini‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ page = Page
1515
template = Template
1616
language = Language
1717
notifications = Notifications
18+
active_stopwatch = Active Time Tracker
1819
create_new = Create…
1920
user_profile_and_more = Profile and Settings…
2021
signed_in_as = Signed in as
@@ -1139,13 +1140,15 @@ issues.lock.title = Lock conversation on this issue.
11391140
issues.unlock.title = Unlock conversation on this issue.
11401141
issues.comment_on_locked = You cannot comment on a locked issue.
11411142
issues.tracker = Time Tracker
1142-
issues.start_tracking_short = Start
1143+
issues.start_tracking_short = Start Timer
11431144
issues.start_tracking = Start Time Tracking
11441145
issues.start_tracking_history = `started working %s`
11451146
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
11461147
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
1147-
issues.stop_tracking = Stop
1148+
issues.stop_tracking = Stop Timer
11481149
issues.stop_tracking_history = `stopped working %s`
1150+
issues.cancel_tracking = Discard
1151+
issues.cancel_tracking_history = `cancelled time tracking %s`
11491152
issues.add_time = Manually Add Time
11501153
issues.add_time_short = Add Time
11511154
issues.add_time_cancel = Cancel
@@ -1154,8 +1157,6 @@ issues.del_time_history= `deleted spent time %s`
11541157
issues.add_time_hours = Hours
11551158
issues.add_time_minutes = Minutes
11561159
issues.add_time_sum_to_small = No time was entered.
1157-
issues.cancel_tracking = Cancel
1158-
issues.cancel_tracking_history = `cancelled time tracking %s`
11591160
issues.time_spent_total = Total Time Spent
11601161
issues.time_spent_from_all_authors = `Total Time Spent: %s`
11611162
issues.due_date = Due Date

‎package-lock.json‎

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"monaco-editor": "0.21.2",
3535
"monaco-editor-webpack-plugin": "2.1.0",
3636
"postcss": "8.2.1",
37+
"pretty-ms": "7.0.1",
3738
"raw-loader": "4.0.2",
3839
"sortablejs": "1.12.0",
3940
"swagger-ui-dist": "3.38.0",

‎routers/repo/issue_stopwatch.go‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package repo
66

77
import (
88
"net/http"
9+
"strings"
910

1011
"code.gitea.io/gitea/models"
1112
"code.gitea.io/gitea/modules/context"
@@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) {
6162
url := issue.HTMLURL()
6263
c.Redirect(url, http.StatusSeeOther)
6364
}
65+
66+
// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
67+
func GetActiveStopwatch(c *context.Context) {
68+
if strings.HasPrefix(c.Req.URL.Path, "/api") {
69+
return
70+
}
71+
72+
if !c.IsSigned {
73+
return
74+
}
75+
76+
_, sw, err := models.HasUserStopwatch(c.User.ID)
77+
if err != nil {
78+
c.ServerError("HasUserStopwatch", err)
79+
return
80+
}
81+
82+
if sw == nil || sw.ID == 0 {
83+
return
84+
}
85+
86+
issue, err := models.GetIssueByID(sw.IssueID)
87+
if err != nil || issue == nil {
88+
c.ServerError("GetIssueByID", err)
89+
return
90+
}
91+
if err = issue.LoadRepo(); err != nil {
92+
c.ServerError("LoadRepo", err)
93+
return
94+
}
95+
96+
c.Data["ActiveStopwatch"] = StopwatchTmplInfo{
97+
issue.Repo.FullName(),
98+
issue.Index,
99+
sw.Seconds() + 1, // ensure time is never zero in ui
100+
}
101+
}
102+
103+
// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
104+
type StopwatchTmplInfo struct {
105+
RepoSlug string
106+
IssueIndex int64
107+
Seconds int64
108+
}

‎routers/routes/macaron.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
176176
}
177177

178178
m.Use(user.GetNotificationCount)
179+
m.Use(repo.GetActiveStopwatch)
179180
m.Use(func(ctx *context.Context) {
180181
ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled()
181182
ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled()

0 commit comments

Comments
 (0)