From b5570d3e680570343c1552bfc972b19b161209cd Mon Sep 17 00:00:00 2001 From: Norwin Date: Thu, 21 Jan 2021 14:51:52 +0000 Subject: [PATCH] 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 --- integrations/api_issue_stopwatch_test.go | 14 ++-- integrations/attachment_test.go | 2 +- models/issue_stopwatch.go | 10 +++ modules/convert/issue.go | 2 + modules/structs/issue_stopwatch.go | 2 + options/locale/locale_en-US.ini | 9 +-- package-lock.json | 13 ++++ package.json | 1 + routers/repo/issue_stopwatch.go | 45 ++++++++++++ routers/routes/macaron.go | 1 + templates/base/head_navbar.tmpl | 38 ++++++++++ templates/repo/issue/new_form.tmpl | 2 +- templates/swagger/v1_json.tmpl | 9 +++ web_src/js/features/stopwatch.js | 91 ++++++++++++++++++++++++ web_src/js/index.js | 2 + 15 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 web_src/js/features/stopwatch.js diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go index 39b9b9741..c0b8fd9c6 100644 --- a/integrations/api_issue_stopwatch_test.go +++ b/integrations/api_issue_stopwatch_test.go @@ -7,7 +7,6 @@ package integrations import ( "net/http" "testing" - "time" "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" @@ -31,14 +30,11 @@ func TestAPIListStopWatches(t *testing.T) { issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) if assert.Len(t, apiWatches, 1) { assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) - apiWatches[0].Created = time.Time{} - assert.EqualValues(t, api.StopWatch{ - Created: time.Time{}, - IssueIndex: issue.Index, - IssueTitle: issue.Title, - RepoName: repo.Name, - RepoOwnerName: repo.OwnerName, - }, *apiWatches[0]) + assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) + assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) + assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) + assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) + assert.Greater(t, int64(apiWatches[0].Seconds), int64(0)) } } diff --git a/integrations/attachment_test.go b/integrations/attachment_test.go index dd734145d..a28e38b99 100644 --- a/integrations/attachment_test.go +++ b/integrations/attachment_test.go @@ -72,7 +72,7 @@ func TestCreateIssueAttachment(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - link, exists := htmlDoc.doc.Find("form").Attr("action") + link, exists := htmlDoc.doc.Find("form#new-issue").Attr("action") assert.True(t, exists, "The template has changed") postData := map[string]string{ diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go index 4b2bf1505..a1c88503d 100644 --- a/models/issue_stopwatch.go +++ b/models/issue_stopwatch.go @@ -19,6 +19,16 @@ type Stopwatch struct { CreatedUnix timeutil.TimeStamp `xorm:"created"` } +// Seconds returns the amount of time passed since creation, based on local server time +func (s Stopwatch) Seconds() int64 { + return int64(timeutil.TimeStampNow() - s.CreatedUnix) +} + +// Duration returns a human-readable duration string based on local server time +func (s Stopwatch) Duration() string { + return SecToTime(s.Seconds()) +} + func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = e. diff --git a/modules/convert/issue.go b/modules/convert/issue.go index 36446da2d..b773e78a6 100644 --- a/modules/convert/issue.go +++ b/modules/convert/issue.go @@ -147,6 +147,8 @@ func ToStopWatches(sws []*models.Stopwatch) (api.StopWatches, error) { result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), + Seconds: sw.Seconds(), + Duration: sw.Duration(), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/modules/structs/issue_stopwatch.go b/modules/structs/issue_stopwatch.go index 8599e0727..15d17cdda 100644 --- a/modules/structs/issue_stopwatch.go +++ b/modules/structs/issue_stopwatch.go @@ -12,6 +12,8 @@ import ( type StopWatch struct { // swagger:strfmt date-time Created time.Time `json:"created"` + Seconds int64 `json:"seconds"` + Duration string `json:"duration"` IssueIndex int64 `json:"issue_index"` IssueTitle string `json:"issue_title"` RepoOwnerName string `json:"repo_owner_name"` diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c950591d7..30fa5f8a7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -15,6 +15,7 @@ page = Page template = Template language = Language notifications = Notifications +active_stopwatch = Active Time Tracker create_new = Create… user_profile_and_more = Profile and Settings… signed_in_as = Signed in as @@ -1139,13 +1140,15 @@ issues.lock.title = Lock conversation on this issue. issues.unlock.title = Unlock conversation on this issue. issues.comment_on_locked = You cannot comment on a locked issue. issues.tracker = Time Tracker -issues.start_tracking_short = Start +issues.start_tracking_short = Start Timer issues.start_tracking = Start Time Tracking issues.start_tracking_history = `started working %s` issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed issues.tracking_already_started = `You have already started time tracking on another issue!` -issues.stop_tracking = Stop +issues.stop_tracking = Stop Timer issues.stop_tracking_history = `stopped working %s` +issues.cancel_tracking = Discard +issues.cancel_tracking_history = `cancelled time tracking %s` issues.add_time = Manually Add Time issues.add_time_short = Add Time issues.add_time_cancel = Cancel @@ -1154,8 +1157,6 @@ issues.del_time_history= `deleted spent time %s` issues.add_time_hours = Hours issues.add_time_minutes = Minutes issues.add_time_sum_to_small = No time was entered. -issues.cancel_tracking = Cancel -issues.cancel_tracking_history = `cancelled time tracking %s` issues.time_spent_total = Total Time Spent issues.time_spent_from_all_authors = `Total Time Spent: %s` issues.due_date = Due Date diff --git a/package-lock.json b/package-lock.json index e20eaf4a3..f3ba4a817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5293,6 +5293,11 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" + }, "parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -6702,6 +6707,14 @@ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "optional": true }, + "pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "requires": { + "parse-ms": "^2.1.0" + } + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", diff --git a/package.json b/package.json index 2abdc5ab7..825237664 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "monaco-editor": "0.21.2", "monaco-editor-webpack-plugin": "2.1.0", "postcss": "8.2.1", + "pretty-ms": "7.0.1", "raw-loader": "4.0.2", "sortablejs": "1.12.0", "swagger-ui-dist": "3.38.0", diff --git a/routers/repo/issue_stopwatch.go b/routers/repo/issue_stopwatch.go index 28105dfe0..b8efb3b84 100644 --- a/routers/repo/issue_stopwatch.go +++ b/routers/repo/issue_stopwatch.go @@ -6,6 +6,7 @@ package repo import ( "net/http" + "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" @@ -61,3 +62,47 @@ func CancelStopwatch(c *context.Context) { url := issue.HTMLURL() c.Redirect(url, http.StatusSeeOther) } + +// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context +func GetActiveStopwatch(c *context.Context) { + if strings.HasPrefix(c.Req.URL.Path, "/api") { + return + } + + if !c.IsSigned { + return + } + + _, sw, err := models.HasUserStopwatch(c.User.ID) + if err != nil { + c.ServerError("HasUserStopwatch", err) + return + } + + if sw == nil || sw.ID == 0 { + return + } + + issue, err := models.GetIssueByID(sw.IssueID) + if err != nil || issue == nil { + c.ServerError("GetIssueByID", err) + return + } + if err = issue.LoadRepo(); err != nil { + c.ServerError("LoadRepo", err) + return + } + + c.Data["ActiveStopwatch"] = StopwatchTmplInfo{ + issue.Repo.FullName(), + issue.Index, + sw.Seconds() + 1, // ensure time is never zero in ui + } +} + +// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering +type StopwatchTmplInfo struct { + RepoSlug string + IssueIndex int64 + Seconds int64 +} diff --git a/routers/routes/macaron.go b/routers/routes/macaron.go index 34978724a..f64a0a597 100644 --- a/routers/routes/macaron.go +++ b/routers/routes/macaron.go @@ -176,6 +176,7 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { } m.Use(user.GetNotificationCount) + m.Use(repo.GetActiveStopwatch) m.Use(func(ctx *context.Context) { ctx.Data["UnitWikiGlobalDisabled"] = models.UnitTypeWiki.UnitGlobalDisabled() ctx.Data["UnitIssuesGlobalDisabled"] = models.UnitTypeIssues.UnitGlobalDisabled() diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index a2b4d4f1d..efab76f33 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -67,6 +67,44 @@ {{else if .IsSigned}}