Illya Marchenko 1 week ago
parent
commit
11b99774c1
No account linked to committer's email address

+ 3
- 0
models/issues/comment.go View File

@@ -112,6 +112,8 @@ const (

CommentTypePin // 36 pin Issue
CommentTypeUnpin // 37 unpin Issue

CommentTypeChangeTimeEstimate // 38 Change time estimate
)

var commentStrings = []string{
@@ -151,6 +153,7 @@ var commentStrings = []string{
"change_issue_ref",
"pull_scheduled_merge",
"pull_cancel_scheduled_merge",
"change_time_estimate",
"pin",
"unpin",
}

+ 33
- 0
models/issues/issue.go View File

@@ -140,6 +140,9 @@ type Issue struct {

// For view issue page.
ShowRole RoleDescriptor `xorm:"-"`

// Time estimate
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}

var (
@@ -909,3 +912,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(issue *Issue, doer *user_model.User, timeEstimate int64) (err error) {
ctx, committer, err := db.TxContext(db.DefaultContext)
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()
}

+ 2
- 0
models/migrations/migrations.go View File

@@ -586,6 +586,8 @@ var migrations = []Migration{
NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode),
// v298 -> v299
NewMigration("Drop wrongly created table o_auth2_application", v1_23.DropWronglyCreatedTable),
// v300 -> v301
NewMigration("Add TimeEstimate to issue table", v1_23.AddTimeEstimateColumnToIssueTable),
}

// GetCurrentDBVersion returns the current db version

+ 16
- 0
models/migrations/v1_23/v299.go View File

@@ -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))
}

+ 8
- 6
modules/templates/helper.go View File

@@ -65,12 +65,14 @@ func NewFuncMap() template.FuncMap {

// -----------------------------------------------------------------
// time / number / format
"FileSize": base.FileSize,
"CountFmt": base.FormatNumberSI,
"TimeSince": timeutil.TimeSince,
"TimeSinceUnix": timeutil.TimeSinceUnix,
"DateTime": timeutil.DateTime,
"Sec2Time": util.SecToTime,
"FileSize": base.FileSize,
"CountFmt": base.FormatNumberSI,
"TimeSince": timeutil.TimeSince,
"TimeSinceUnix": timeutil.TimeSinceUnix,
"DateTime": timeutil.DateTime,
"Sec2Time": util.SecToTime,
"SecToTimeExact": util.SecToTimeExact,
"TimeEstimateToStr": util.TimeEstimateToStr,
"LoadTimes": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
},

+ 37
- 0
modules/util/sec_to_time.go View File

@@ -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"

+ 99
- 0
modules/util/time_str.go View File

@@ -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, " ")
}

+ 9
- 4
options/locale/locale_en-US.ini View File

@@ -1480,6 +1480,11 @@ issues.add_assignee_at = `was assigned by <b>%s</b> %s`
issues.remove_assignee_at = `was unassigned by <b>%s</b> %s`
issues.remove_self_assignment = `removed their assignment %s`
issues.change_title_at = `changed title from <b><strike>%s</strike></b> to <b>%s</b> %s`
issues.time_estimate = `Time Estimate`
issues.add_time_estimate = `3w 4d 12h`
issues.change_time_estimate_at = `changed time estimate to <b>%s</b> %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 <b><strike>%s</strike></b> to <b>%s</b> %s`
issues.remove_ref_at = `removed reference <b>%s</b> %s`
issues.add_ref_at = `added reference <b>%s</b> %s`
@@ -1650,20 +1655,20 @@ 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 <a href="%s">another issue</a>!`
issues.stop_tracking = Stop Timer
issues.stop_tracking_history = `stopped working %s`
issues.stop_tracking_history = `worked for <b>%s</b> %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 <b>%s</b> %s`
issues.del_time_history= `deleted spent time <b>%s</b> %s`
issues.add_time_hours = Hours
issues.add_time_minutes = Minutes
issues.add_time_sum_to_small = No time was entered.
issues.time_spent_total = Total Time Spent
issues.time_spent_from_all_authors = `Total Time Spent: %s`
issues.time_spent_from_all_authors = `Total Time Spent:`
issues.due_date = Due Date
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
issues.error_modifying_due_date = "Failed to modify the due date."

BIN
public/assets/img/webpack/jquery.minicolors.0e614115.png View File


+ 55
- 0
routers/web/repo/issue.go View File

@@ -13,6 +13,7 @@ import (
"math/big"
"net/http"
"net/url"
"regexp"
"slices"
"sort"
"strconv"
@@ -1771,6 +1772,9 @@ func ViewIssue(ctx *context.Context) {
comment.Content = comment.Content[1:]
}
}
} else if comment.Type == issues_model.CommentTypeChangeTimeEstimate {
timeSec, _ := util.ToInt64(comment.Content)
comment.Content = util.SecToTimeExact(timeSec, timeSec < 60)
}

if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
@@ -2208,6 +2212,57 @@ func UpdateIssueTitle(ctx *context.Context) {
})
}

// 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(issue, ctx.Doer, total); err != nil {
ctx.ServerError("ChangeTimeEstimate", err)
return
}

ctx.Redirect(url, http.StatusSeeOther)
}

// UpdateIssueRef change issue's ref (branch)
func UpdateIssueRef(ctx *context.Context) {
issue := GetActionIssue(ctx)

+ 2
- 2
routers/web/repo/issue_timetrack.go View File

@@ -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
}

+ 1
- 0
routers/web/web.go View File

@@ -1202,6 +1202,7 @@ func registerRoutes(m *web.Route) {
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)

+ 5
- 0
services/convert/issue_comment.go View File

@@ -76,6 +76,11 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
// so we check for the "|" delimeter 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{

+ 1
- 2
services/forms/repo_form.go View File

@@ -877,8 +877,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

+ 1
- 0
services/forms/user_form_hidden_comments.go View File

@@ -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,

+ 2
- 57
services/issue/commit.go View File

@@ -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
}

+ 7
- 0
services/issue/issue.go View File

@@ -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(issue *issues_model.Issue, doer *user_model.User, timeEstimate int64) (err error) {
issue.TimeEstimate = timeEstimate

return issues_model.ChangeIssueTimeEstimate(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

+ 1
- 1
templates/mail/issue/default.tmpl View File

@@ -62,7 +62,7 @@
{{end -}}
{{- range .ReviewComments}}
<hr>
{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}
{{ctx.Locale.Tr "mail.issue.in_tree_path" .TreePath}}
<div class="review">
<pre>{{.Patch}}</pre>
<div>{{.RenderedContent}}</div>

+ 27
- 17
templates/repo/issue/view_content/comments.tmpl View File

@@ -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}}
<div class="timeline-item comment" id="{{.HashTag}}">
{{if .OriginalAuthor}}
@@ -249,18 +250,18 @@
{{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
<div class="detail flex-text-block">
{{svg "octicon-clock"}}

{{$timeStr := ""}}
{{if .RenderedContent}}
{{/* compatibility with time comments made before v1.21 */}}
<span class="text grey muted-links">{{.RenderedContent}}</span>
{{$timeStr = .RenderedContent}}
{{else}}
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
{{$timeStr = .Content|Sec2Time}}
{{end}}
</div>

{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr | SafeHTML}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
</div>
{{else if eq .Type 14}}
<div class="timeline-item event" id="{{.HashTag}}">
@@ -268,18 +269,18 @@
{{template "shared/user/avatarlink" dict "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
<div class="detail flex-text-block">
{{svg "octicon-clock"}}

{{$timeStr := ""}}
{{if .RenderedContent}}
{{/* compatibility with time comments made before v1.21 */}}
<span class="text grey muted-links">{{.RenderedContent}}</span>
{{$timeStr = .RenderedContent}}
{{else}}
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
{{$timeStr = .Content|Sec2Time}}
{{end}}
</div>

{{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr | SafeHTML}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
</div>
{{else if eq .Type 15}}
<div class="timeline-item event" id="{{.HashTag}}">
@@ -680,6 +681,15 @@
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
</span>
</div>
{{else if eq .Type 38}}
<div class="timeline-item event" id="{{.HashTag}}">
<span class="badge">{{svg "octicon-clock"}}</span>
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{ctx.Locale.Tr "repo.issues.change_time_estimate_at" .Content $createdStr | SafeHTML}}
</span>
</div>
{{end}}
{{end}}
{{end}}

+ 21
- 7
templates/repo/issue/view_content/sidebar.tmpl View File

@@ -278,8 +278,22 @@
{{end}}
{{if .Repository.IsTimetrackerEnabled $.Context}}
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div>
<div class="ui timetrack">
<div class="ui divider"></div>
<div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_estimate"}}</strong></span>

<form method="post" id="set_time_estimate_form" class="gt-mt-3" action="{{.Issue.Link}}/time_estimate">
{{$.CsrfTokenHtml}}
<div class="ui input fluid">
<input name="time_estimate" placeholder='{{ctx.Locale.Tr "repo.issues.add_time_estimate"}}' value="{{TimeEstimateToStr .Issue.TimeEstimate}}" data-value="{{$.Issue.TimeEstimate}}" type="text" >
</div>
<button class="ui fluid button green tooltip tw-mt-1">
{{ctx.Locale.Tr "repo.issues.save"}}
</button>
</form>
</div>
<div class="divider"></div>
<div class="ui timetrack">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
<div class="tw-mt-2">
<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
@@ -311,9 +325,8 @@
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
<div class="content">
<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
{{$.CsrfTokenHtml}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
{{$.CsrfTokenHtml}}
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_estimate"}}' type="text" name="time_string">
</form>
</div>
<div class="actions">
@@ -332,8 +345,9 @@
{{if .WorkingUsers}}
<div class="divider"></div>
<div class="ui comments">
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
<div>
<div class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" | SafeHTML}}</strong></div>
<div>{{SecToTimeExact .Issue.TotalTrackedTime false}}</div>
<div class="gt-mt-3">
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2">
<a class="avatar">

+ 1
- 2
tests/integration/timetracking_test.go View File

@@ -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)
}

Loading…
Cancel
Save