diff --git a/models/issues/comment.go b/models/issues/comment.go index 48b8e335d48ef..7065712bfd231 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -114,6 +114,8 @@ const ( CommentTypePin // 36 pin Issue CommentTypeUnpin // 37 unpin Issue + + CommentTypeChangeTimeEstimate // 38 Change time estimate ) var commentStrings = []string{ @@ -153,6 +155,7 @@ var commentStrings = []string{ "change_issue_ref", "pull_scheduled_merge", "pull_cancel_scheduled_merge", + "change_time_estimate", "pin", "unpin", } diff --git a/models/issues/issue.go b/models/issues/issue.go index 9ccf2859ea413..d4992f7f92668 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -147,6 +147,9 @@ type Issue struct { // For view issue page. ShowRole RoleDescriptor `xorm:"-"` + + // Time estimate + TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` } var ( @@ -934,3 +937,33 @@ func insertIssue(ctx context.Context, issue *Issue) error { return nil } + +// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user. +func ChangeIssueTimeEstimate(ctx context.Context, issue *Issue, doer *user_model.User, timeEstimate int64) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil { + return fmt.Errorf("updateIssueCols: %w", err) + } + + if err = issue.LoadRepo(ctx); err != nil { + return fmt.Errorf("loadRepo: %w", err) + } + + opts := &CreateCommentOptions{ + Type: CommentTypeChangeTimeEstimate, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Content: fmt.Sprintf("%d", timeEstimate), + } + if _, err = CreateComment(ctx, opts); err != nil { + return fmt.Errorf("createComment: %w", err) + } + + return committer.Commit() +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e0361af86ba8e..8ccea29ebf54a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -367,6 +367,7 @@ func prepareMigrationTasks() []*migration { newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate), newMigration(308, "Add index(user_id, is_deleted) for action table", v1_23.AddNewIndexForUserDashboard), newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), + newMigration(310, "Improve Notification table indices", v1_23.AddTimeEstimateColumnToIssueTable), } return preparedMigrations } diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go new file mode 100644 index 0000000000000..0fc1ac8c0e85c --- /dev/null +++ b/models/migrations/v1_23/v310.go @@ -0,0 +1,16 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "xorm.io/xorm" +) + +func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error { + type Issue struct { + TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync(new(Issue)) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d5b32358da752..4b815e8729949 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -67,9 +67,11 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format - "FileSize": base.FileSize, - "CountFmt": base.FormatNumberSI, - "Sec2Time": util.SecToTime, + "FileSize": base.FileSize, + "CountFmt": base.FormatNumberSI, + "Sec2Time": util.SecToTime, + "SecToTimeExact": util.SecToTimeExact, + "TimeEstimateToStr": util.TimeEstimateToStr, "LoadTimes": func(startTime time.Time) string { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" }, diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b4a7..aede619ae963d 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -66,6 +66,43 @@ func SecToTime(durationVal any) string { return strings.TrimRight(formattedTime, " ") } +func SecToTimeExact(duration int64, withSeconds bool) string { + 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 + + // Show exact time information + formattedTime = formatTime(years, "year", formattedTime) + formattedTime = formatTime(months, "month", formattedTime) + formattedTime = formatTime(weeks, "week", formattedTime) + formattedTime = formatTime(days, "day", formattedTime) + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) + if withSeconds { + formattedTime = formatTime(seconds, "second", formattedTime) + } + + // The formatTime() function always appends a space at the end. This will be trimmed + return strings.TrimRight(formattedTime, " ") +} + // formatTime appends the given value to the existing forammattedTime. E.g: // formattedTime = "1 year" // input: value = 3, name = "month" diff --git a/modules/util/time_str.go b/modules/util/time_str.go new file mode 100644 index 0000000000000..02a4a82e6bd99 --- /dev/null +++ b/modules/util/time_str.go @@ -0,0 +1,99 @@ +// Copyright 2022 Gitea. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +var ( + // Time estimate match regex + rTimeEstimateOnlyHours = regexp.MustCompile(`^([\d]+)$`) + rTimeEstimateWeeks = regexp.MustCompile(`([\d]+)w`) + rTimeEstimateDays = regexp.MustCompile(`([\d]+)d`) + rTimeEstimateHours = regexp.MustCompile(`([\d]+)h`) + rTimeEstimateMinutes = regexp.MustCompile(`([\d]+)m`) +) + +// TimeEstimateFromStr returns time estimate in seconds from formatted string +func TimeEstimateFromStr(timeStr string) int64 { + timeTotal := 0 + + // If single number entered, assume hours + timeStrMatches := rTimeEstimateOnlyHours.FindStringSubmatch(timeStr) + if len(timeStrMatches) > 0 { + raw, _ := strconv.Atoi(timeStrMatches[1]) + timeTotal += raw * (60 * 60) + } else { + // Find time weeks + timeStrMatches = rTimeEstimateWeeks.FindStringSubmatch(timeStr) + if len(timeStrMatches) > 0 { + raw, _ := strconv.Atoi(timeStrMatches[1]) + timeTotal += raw * (60 * 60 * 24 * 7) + } + + // Find time days + timeStrMatches = rTimeEstimateDays.FindStringSubmatch(timeStr) + if len(timeStrMatches) > 0 { + raw, _ := strconv.Atoi(timeStrMatches[1]) + timeTotal += raw * (60 * 60 * 24) + } + + // Find time hours + timeStrMatches = rTimeEstimateHours.FindStringSubmatch(timeStr) + if len(timeStrMatches) > 0 { + raw, _ := strconv.Atoi(timeStrMatches[1]) + timeTotal += raw * (60 * 60) + } + + // Find time minutes + timeStrMatches = rTimeEstimateMinutes.FindStringSubmatch(timeStr) + if len(timeStrMatches) > 0 { + raw, _ := strconv.Atoi(timeStrMatches[1]) + timeTotal += raw * (60) + } + } + + return int64(timeTotal) +} + +// TimeEstimateStr returns formatted time estimate string from seconds (e.g. "2w 4d 12h 5m") +func TimeEstimateToStr(amount int64) string { + var timeParts []string + + timeSeconds := float64(amount) + + // Format weeks + weeks := math.Floor(timeSeconds / (60 * 60 * 24 * 7)) + if weeks > 0 { + timeParts = append(timeParts, fmt.Sprintf("%dw", int64(weeks))) + } + timeSeconds -= weeks * (60 * 60 * 24 * 7) + + // Format days + days := math.Floor(timeSeconds / (60 * 60 * 24)) + if days > 0 { + timeParts = append(timeParts, fmt.Sprintf("%dd", int64(days))) + } + timeSeconds -= days * (60 * 60 * 24) + + // Format hours + hours := math.Floor(timeSeconds / (60 * 60)) + if hours > 0 { + timeParts = append(timeParts, fmt.Sprintf("%dh", int64(hours))) + } + timeSeconds -= hours * (60 * 60) + + // Format minutes + minutes := math.Floor(timeSeconds / (60)) + if minutes > 0 { + timeParts = append(timeParts, fmt.Sprintf("%dm", int64(minutes))) + } + + return strings.Join(timeParts, " ") +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9945eb4949d68..f6fbe5a56bd06 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1506,6 +1506,11 @@ issues.add_assignee_at = `was assigned by %s %s` issues.remove_assignee_at = `was unassigned by %s %s` issues.remove_self_assignment = `removed their assignment %s` issues.change_title_at = `changed title from %s to %s %s` +issues.time_estimate = `Time Estimate` +issues.add_time_estimate = `3w 4d 12h` +issues.change_time_estimate_at = `changed time estimate to %s %s` +issues.remove_time_estimate = `removed time estimate %s` +issues.time_estimate_invalid = `Time estimate format is invalid` issues.change_ref_at = `changed reference from %s to %s %s` issues.remove_ref_at = `removed reference %s %s` issues.add_ref_at = `added reference %s %s` @@ -1676,15 +1681,15 @@ 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 Timer -issues.stop_tracking_history = `stopped working %s` +issues.stop_tracking_history = `worked for %s %s` issues.cancel_tracking = Discard issues.cancel_tracking_history = `canceled time tracking %s` issues.add_time = Manually Add Time issues.del_time = Delete this time log issues.add_time_short = Add Time issues.add_time_cancel = Cancel -issues.add_time_history = `added spent time %s` -issues.del_time_history= `deleted spent time %s` +issues.add_time_history = `added spent time %s %s` +issues.del_time_history= `deleted spent time %s %s` issues.add_time_hours = Hours issues.add_time_minutes = Minutes issues.add_time_sum_to_small = No time was entered. diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index 9a941ce85714b..08e463ff50e10 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -8,6 +8,7 @@ import ( "fmt" "html/template" "net/http" + "regexp" "slices" "sort" "strconv" @@ -401,3 +402,54 @@ func NewIssuePost(ctx *context.Context) { ctx.JSONRedirect(issue.Link()) } } + +// UpdateIssueTimeEstimate change issue's planned time +var ( + rTimeEstimateStr = regexp.MustCompile(`^([\d]+w)?\s?([\d]+d)?\s?([\d]+h)?\s?([\d]+m)?$`) + rTimeEstimateStrHoursOnly = regexp.MustCompile(`^([\d]+)$`) +) + +func UpdateIssueTimeEstimate(ctx *context.Context) { + issue := GetActionIssue(ctx) + if ctx.Written() { + return + } + + if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { + ctx.Error(http.StatusForbidden) + return + } + + url := issue.Link() + + timeStr := ctx.FormString("time_estimate") + + // Validate input + if !rTimeEstimateStr.MatchString(timeStr) && !rTimeEstimateStrHoursOnly.MatchString(timeStr) { + ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid")) + ctx.Redirect(url, http.StatusSeeOther) + return + } + + total := util.TimeEstimateFromStr(timeStr) + + // User entered something wrong + if total == 0 && len(timeStr) != 0 { + ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid")) + ctx.Redirect(url, http.StatusSeeOther) + return + } + + // No time changed + if issue.TimeEstimate == total { + ctx.Redirect(url, http.StatusSeeOther) + return + } + + if err := issue_service.ChangeTimeEstimate(ctx, issue, ctx.Doer, total); err != nil { + ctx.ServerError("ChangeTimeEstimate", err) + return + } + + ctx.Redirect(url, http.StatusSeeOther) +} diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 099711c5e9cf8..e54bd32e9486c 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -34,7 +34,7 @@ func AddTimeManually(c *context.Context) { return } - total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute + total := util.TimeEstimateFromStr(form.TimeString) if total <= 0 { c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small")) @@ -42,7 +42,7 @@ func AddTimeManually(c *context.Context) { return } - if _, err := issues_model.AddTime(c, c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil { + if _, err := issues_model.AddTime(c, c.Doer, issue, total, time.Now()); err != nil { c.ServerError("AddTime", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index b96d06ed66eb6..69e73e3d67643 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1232,6 +1232,7 @@ func registerRoutes(m *web.Router) { m.Post("/cancel", repo.CancelStopwatch) }) }) + m.Post("/time_estimate", repo.UpdateIssueTimeEstimate) m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction) m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue) m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue) diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index 9ec9ac7684a1d..961f5690434f0 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -76,6 +76,11 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu // so we check for the "|" delimiter and convert new to legacy format on demand c.Content = util.SecToTime(c.Content[1:]) } + + if c.Type == issues_model.CommentTypeChangeTimeEstimate { + timeSec, _ := util.ToInt64(c.Content) + c.Content = util.SecToTimeExact(timeSec, timeSec < 60) + } } comment := &api.TimelineComment{ diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 8e663084f86e1..40bce039508b6 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -847,8 +847,7 @@ func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) bi // AddTimeManuallyForm form that adds spent time manually. type AddTimeManuallyForm struct { - Hours int `binding:"Range(0,1000)"` - Minutes int `binding:"Range(0,1000)"` + TimeString string } // Validate validates the fields diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index b9677c1800631..76382ddfdde68 100644 --- a/services/forms/user_form_hidden_comments.go +++ b/services/forms/user_form_hidden_comments.go @@ -43,6 +43,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{ /*14*/ issues_model.CommentTypeAddTimeManual, /*15*/ issues_model.CommentTypeCancelTracking, /*26*/ issues_model.CommentTypeDeleteTimeManual, + /*38*/ issues_model.CommentTypeChangeTimeEstimate, }, "deadline": { /*16*/ issues_model.CommentTypeAddedDeadline, diff --git a/services/issue/commit.go b/services/issue/commit.go index 0579e0f5c53e6..ed80be328bae4 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -9,8 +9,6 @@ import ( "fmt" "html" "net/url" - "regexp" - "strconv" "strings" "time" @@ -23,64 +21,11 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" ) -const ( - secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute - secondsByHour = 60 * secondsByMinute // seconds in an hour - secondsByDay = 8 * secondsByHour // seconds in a day - secondsByWeek = 5 * secondsByDay // seconds in a week - secondsByMonth = 4 * secondsByWeek // seconds in a month -) - -var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`) - -// timeLogToAmount parses time log string and returns amount in seconds -func timeLogToAmount(str string) int64 { - matches := reDuration.FindAllStringSubmatch(str, -1) - if len(matches) == 0 { - return 0 - } - - match := matches[0] - - var a int64 - - // months - if len(match[1]) > 0 { - mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64) - a += int64(mo * secondsByMonth) - } - - // weeks - if len(match[3]) > 0 { - w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64) - a += int64(w * secondsByWeek) - } - - // days - if len(match[5]) > 0 { - d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64) - a += int64(d * secondsByDay) - } - - // hours - if len(match[7]) > 0 { - h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64) - a += int64(h * secondsByHour) - } - - // minutes - if len(match[9]) > 0 { - d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64) - a += int64(d * secondsByMinute) - } - - return a -} - func issueAddTime(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, time time.Time, timeLog string) error { - amount := timeLogToAmount(timeLog) + amount := util.TimeEstimateFromStr(timeLog) if amount == 0 { return nil } diff --git a/services/issue/issue.go b/services/issue/issue.go index 72ea66c8d98c5..c6a52cc0fe512 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -105,6 +105,13 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode return nil } +// ChangeTimeEstimate changes the time estimate of this issue, as the given user. +func ChangeTimeEstimate(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, timeEstimate int64) (err error) { + issue.TimeEstimate = timeEstimate + + return issues_model.ChangeIssueTimeEstimate(ctx, issue, doer, timeEstimate) +} + // ChangeIssueRef changes the branch of this issue, as the given user. func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error { oldRef := issue.Ref diff --git a/templates/mail/issue/default.tmpl b/templates/mail/issue/default.tmpl index 395b118d3ef22..7859a94e1014b 100644 --- a/templates/mail/issue/default.tmpl +++ b/templates/mail/issue/default.tmpl @@ -62,7 +62,7 @@ {{end -}} {{- range .ReviewComments}}
- {{$.locale.Tr "mail.issue.in_tree_path" .TreePath}} + {{ctx.Locale.Tr "mail.issue.in_tree_path" .TreePath}}
{{.Patch}}
{{.RenderedContent}}
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl index 9a49664b0e6f1..7a96f8d49f8ad 100644 --- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl +++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl @@ -1,5 +1,19 @@ {{if .Repository.IsTimetrackerEnabled ctx}} {{if and .CanUseTimetracker (not .Repository.IsArchived)}} +
+
+ {{ctx.Locale.Tr "repo.issues.time_estimate"}} + +
+ {{$.CsrfTokenHtml}} +
+ +
+ +
+
{{ctx.Locale.Tr "repo.issues.tracker"}} @@ -34,8 +48,7 @@
{{$.CsrfTokenHtml}} - - +
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 47551c86e473d..dd01202d86111 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -12,7 +12,8 @@ 26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST, 29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED 32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE, - 35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE --> + 35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE, + 38 = COMMENT_TYPE_CHANGE_TIME_ESTIMATE --> {{if eq .Type 0}}
{{if .OriginalAuthor}} @@ -250,18 +251,18 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} + + {{$timeStr := ""}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} + {{$timeStr = .RenderedContent}} {{else}} - {{.Content|Sec2Time}} + {{$timeStr = .Content|Sec2Time}} {{end}} -
+ + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr | SafeHTML}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
{{else if eq .Type 14}}
@@ -269,18 +270,18 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}} - - {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} + + {{$timeStr := ""}} {{if .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} + {{$timeStr = .RenderedContent}} {{else}} - {{.Content|Sec2Time}} + {{$timeStr = .Content|Sec2Time}} {{end}} -
+ + {{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr | SafeHTML}} + + {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
{{else if eq .Type 15}}
@@ -703,6 +704,22 @@ {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
+ {{else if eq .Type 38}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + + {{if .RenderedContent}} + {{/* compatibility with time comments made before v1.21 */}} + {{ctx.Locale.Tr "repo.issues.change_time_estimate_at" .RenderedContent $createdStr | SafeHTML}} + {{else}} + {{$timeStr := .Content|Sec2Time}} + {{ctx.Locale.Tr "repo.issues.change_time_estimate_at" $timeStr $createdStr | SafeHTML}} + {{end}} + +
{{end}} {{end}} {{end}} diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go index 10e539cbe6197..0f593dade48ec 100644 --- a/tests/integration/timetracking_test.go +++ b/tests/integration/timetracking_test.go @@ -73,8 +73,7 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo htmlDoc = NewHTMLParser(t, resp.Body) events = htmlDoc.doc.Find(".event > span.text") - assert.Contains(t, events.Last().Text(), "stopped working") - htmlDoc.AssertElement(t, ".event .detail .octicon-clock", true) + assert.Contains(t, events.Last().Text(), "worked for ") } else { session.MakeRequest(t, req, http.StatusNotFound) }