From eef635523a8890ec6fb80eb2a67968d439f440ba Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 21 Jan 2025 06:49:58 +0800 Subject: [PATCH] Make tracked time representation display as hours (#33315) (#33334) Try to backport #33315, the only trivial conflict is in the helper functions map in the helper.go Fix #33333 Co-authored-by: Sysoev, Vladimir --- models/issues/stopwatch.go | 4 +- modules/templates/helper.go | 2 +- modules/util/sec_to_time.go | 59 ++++------------------------- modules/util/sec_to_time_test.go | 21 ++++------ routers/web/repo/issue_timetrack.go | 2 +- services/convert/issue.go | 3 +- services/convert/issue_comment.go | 2 +- web_src/js/features/stopwatch.ts | 27 ++++--------- 8 files changed, 30 insertions(+), 90 deletions(-) diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 629af95b57..baffe04ace 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -48,7 +48,7 @@ func (s Stopwatch) Seconds() int64 { // Duration returns a human-readable duration string based on local server time func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) + return util.SecToHours(s.Seconds()) } func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { @@ -201,7 +201,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: util.SecToHours(timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index ff9673ccef..0b78defac9 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -70,7 +70,7 @@ func NewFuncMap() template.FuncMap { // time / number / format "FileSize": base.FileSize, "CountFmt": base.FormatNumberSI, - "Sec2Time": util.SecToTime, + "Sec2Time": util.SecToHours, "TimeEstimateString": timeEstimateString, diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b..73667d723e 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -8,59 +8,17 @@ import ( "strings" ) -// SecToTime converts an amount of seconds to a human-readable string. E.g. -// 66s -> 1 minute 6 seconds -// 52410s -> 14 hours 33 minutes -// 563418 -> 6 days 12 hours -// 1563418 -> 2 weeks 4 days -// 3937125s -> 1 month 2 weeks -// 45677465s -> 1 year 6 months -func SecToTime(durationVal any) string { +// SecToHours converts an amount of seconds to a human-readable hours string. +// This is stable for planning and managing timesheets. +// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours. +func SecToHours(durationVal any) string { duration, _ := ToInt64(durationVal) + hours := duration / 3600 + minutes := (duration / 60) % 60 formattedTime := "" - - // The following four variables are calculated by taking - // into account the previously calculated variables, this avoids - // pitfalls when using remainders. As that could lead to incorrect - // results when the calculated number equals the quotient number. - remainingDays := duration / (60 * 60 * 24) - years := remainingDays / 365 - remainingDays -= years * 365 - months := remainingDays * 12 / 365 - remainingDays -= months * 365 / 12 - weeks := remainingDays / 7 - remainingDays -= weeks * 7 - days := remainingDays - - // The following three variables are calculated without depending - // on the previous calculated variables. - hours := (duration / 3600) % 24 - minutes := (duration / 60) % 60 - seconds := duration % 60 - - // Extract only the relevant information of the time - // If the time is greater than a year, it makes no sense to display seconds. - switch { - case years > 0: - formattedTime = formatTime(years, "year", formattedTime) - formattedTime = formatTime(months, "month", formattedTime) - case months > 0: - formattedTime = formatTime(months, "month", formattedTime) - formattedTime = formatTime(weeks, "week", formattedTime) - case weeks > 0: - formattedTime = formatTime(weeks, "week", formattedTime) - formattedTime = formatTime(days, "day", formattedTime) - case days > 0: - formattedTime = formatTime(days, "day", formattedTime) - formattedTime = formatTime(hours, "hour", formattedTime) - case hours > 0: - formattedTime = formatTime(hours, "hour", formattedTime) - formattedTime = formatTime(minutes, "minute", formattedTime) - default: - formattedTime = formatTime(minutes, "minute", formattedTime) - formattedTime = formatTime(seconds, "second", formattedTime) - } + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) // The formatTime() function always appends a space at the end. This will be trimmed return strings.TrimRight(formattedTime, " ") @@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string { } else if value > 1 { formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name) } - return formattedTime } diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go index 4d1213a52c..71a8801d4f 100644 --- a/modules/util/sec_to_time_test.go +++ b/modules/util/sec_to_time_test.go @@ -9,22 +9,17 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSecToTime(t *testing.T) { +func TestSecToHours(t *testing.T) { second := int64(1) minute := 60 * second hour := 60 * minute day := 24 * hour - year := 365 * day - assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second)) - assert.Equal(t, "1 hour", SecToTime(hour)) - assert.Equal(t, "1 hour", SecToTime(hour+second)) - assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second)) - assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second)) - assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second)) - assert.Equal(t, "4 weeks", SecToTime(4*7*day)) - assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day)) - assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second)) - assert.Equal(t, "11 months", SecToTime(year-25*day)) - assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second)) + assert.Equal(t, "1 minute", SecToHours(minute+6*second)) + assert.Equal(t, "1 hour", SecToHours(hour)) + assert.Equal(t, "1 hour", SecToHours(hour+second)) + assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second)) + assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second)) + assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second)) + assert.Equal(t, "672 hours", SecToHours(4*7*day)) } diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 88c539488e..11ffa8bacf 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) { return } - c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) + c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time))) c.JSONRedirect("") } diff --git a/services/convert/issue.go b/services/convert/issue.go index e3124efd64..37935accca 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { @@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), Seconds: sw.Seconds(), - Duration: sw.Duration(), + Duration: util.SecToHours(sw.Seconds()), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index b8527ae233..9ad584a62f 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu c.Content[0] == '|' { // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string // so we check for the "|" delimiter and convert new to legacy format on demand - c.Content = util.SecToTime(c.Content[1:]) + c.Content = util.SecToHours(c.Content[1:]) } if c.Type == issues_model.CommentTypeChangeTimeEstimate { diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index af52be4e24..46168b2cd7 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -1,6 +1,6 @@ import {createTippy} from '../modules/tippy.ts'; import {GET} from '../modules/fetch.ts'; -import {hideElem, showElem} from '../utils/dom.ts'; +import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; @@ -144,23 +144,10 @@ function updateStopwatchData(data) { return Boolean(data.length); } -// TODO: This flickers on page load, we could avoid this by making a custom -// element to render time periods. Feeding a datetime in backend does not work -// when time zone between server and client differs. -function updateStopwatchTime(seconds) { - if (!Number.isFinite(seconds)) return; - const datetime = (new Date(Date.now() - seconds * 1000)).toISOString(); - for (const parent of document.querySelectorAll('.header-stopwatch-dot')) { - const existing = parent.querySelector(':scope > relative-time'); - if (existing) { - existing.setAttribute('datetime', datetime); - } else { - const el = document.createElement('relative-time'); - el.setAttribute('format', 'micro'); - el.setAttribute('datetime', datetime); - el.setAttribute('lang', 'en-US'); - el.setAttribute('title', ''); // make show no title and therefor no tooltip - parent.append(el); - } - } +// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods. +function updateStopwatchTime(seconds: number) { + const hours = seconds / 3600 || 0; + const minutes = seconds / 60 || 0; + const timeText = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`; + queryElems(document, '.header-stopwatch-dot', (el) => el.textContent = timeText); }