From 936665bf857787fb653a40dd202d0333c14e0e7e Mon Sep 17 00:00:00 2001 From: Illya Marchenko Date: Thu, 5 Dec 2024 15:07:53 +0200 Subject: [PATCH] Issue time estimate, meaningful time tracking (#23113) Redesign the time tracker side bar, and add "time estimate" support (in "1d 2m" format) Closes #23112 --------- Co-authored-by: stuzer05 Co-authored-by: Yarden Shoham Co-authored-by: silverwind Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang --- models/issues/comment.go | 3 + models/issues/issue.go | 28 +++++ models/migrations/migrations.go | 1 + models/migrations/v1_23/v311.go | 16 +++ modules/templates/helper.go | 11 ++ modules/util/time_str.go | 85 ++++++++++++++ modules/util/time_str_test.go | 55 +++++++++ options/locale/locale_en-US.ini | 27 +++-- routers/web/repo/issue_stopwatch.go | 7 +- routers/web/repo/issue_timetrack.go | 46 ++++++-- routers/web/web.go | 1 + services/convert/issue_comment.go | 5 + services/forms/user_form_hidden_comments.go | 1 + services/issue/issue.go | 7 ++ .../issue/sidebar/stopwatch_timetracker.tmpl | 110 ++++++++++-------- .../repo/issue/view_content/comments.tmpl | 43 +++---- .../view_content/comments_delete_time.tmpl | 12 +- tests/integration/html_helper.go | 8 +- tests/integration/timetracking_test.go | 55 ++++----- web_src/js/features/repo-issue.ts | 31 ----- web_src/js/index.ts | 2 - 21 files changed, 390 insertions(+), 164 deletions(-) create mode 100644 models/migrations/v1_23/v311.go create mode 100644 modules/util/time_str.go create mode 100644 modules/util/time_str_test.go diff --git a/models/issues/comment.go b/models/issues/comment.go index 6650817e50..e4537aa872 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{ @@ -155,6 +157,7 @@ var commentStrings = []string{ "pull_cancel_scheduled_merge", "pin", "unpin", + "change_time_estimate", } func (t CommentType) String() string { diff --git a/models/issues/issue.go b/models/issues/issue.go index fd89267b84..64fc20cc05 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,28 @@ 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) error { + return db.WithTx(ctx, func(ctx context.Context) error { + 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 nil + }) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 4c3cefde7b..52d10c4fe8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -368,6 +368,7 @@ func prepareMigrationTasks() []*migration { 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, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch), + newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable), } return preparedMigrations } diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go new file mode 100644 index 0000000000..0fc1ac8c0e --- /dev/null +++ b/models/migrations/v1_23/v311.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 d5b32358da..e6442fa87e 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -70,6 +70,9 @@ func NewFuncMap() template.FuncMap { "FileSize": base.FileSize, "CountFmt": base.FormatNumberSI, "Sec2Time": util.SecToTime, + + "TimeEstimateString": timeEstimateString, + "LoadTimes": func(startTime time.Time) string { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" }, @@ -282,6 +285,14 @@ func userThemeName(user *user_model.User) string { return setting.UI.DefaultTheme } +func timeEstimateString(timeSec any) string { + v, _ := util.ToInt64(timeSec) + if v == 0 { + return "" + } + return util.TimeEstimateString(v) +} + func panicIfDevOrTesting() { if !setting.IsProd || setting.IsInTesting { panic("legacy template functions are for backward compatibility only, do not use them in new code") diff --git a/modules/util/time_str.go b/modules/util/time_str.go new file mode 100644 index 0000000000..b56f4740af --- /dev/null +++ b/modules/util/time_str.go @@ -0,0 +1,85 @@ +// Copyright 2024 Gitea. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "sync" +) + +type timeStrGlobalVarsType struct { + units []struct { + name string + num int64 + } + re *regexp.Regexp +} + +// When tracking working time, only hour/minute/second units are accurate and could be used. +// For other units like "day", it depends on "how many working hours in a day": 6 or 7 or 8? +// So at the moment, we only support hour/minute/second units. +// In the future, it could be some configurable options to help users +// to convert the working time to different units. + +var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType { + v := &timeStrGlobalVarsType{} + v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`) + v.units = []struct { + name string + num int64 + }{ + {"h", 60 * 60}, + {"m", 60}, + {"s", 1}, + } + return v +}) + +func TimeEstimateParse(timeStr string) (int64, error) { + if timeStr == "" { + return 0, nil + } + var total int64 + matches := timeStrGlobalVars().re.FindAllStringSubmatchIndex(timeStr, -1) + if len(matches) == 0 { + return 0, fmt.Errorf("invalid time string: %s", timeStr) + } + if matches[0][0] != 0 || matches[len(matches)-1][1] != len(timeStr) { + return 0, fmt.Errorf("invalid time string: %s", timeStr) + } + for _, match := range matches { + amount, err := strconv.ParseInt(timeStr[match[2]:match[3]], 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid time string: %v", err) + } + unit := timeStr[match[4]:match[5]] + found := false + for _, u := range timeStrGlobalVars().units { + if strings.ToLower(unit) == u.name { + total += amount * u.num + found = true + break + } + } + if !found { + return 0, fmt.Errorf("invalid time unit: %s", unit) + } + } + return total, nil +} + +func TimeEstimateString(amount int64) string { + var timeParts []string + for _, u := range timeStrGlobalVars().units { + if amount >= u.num { + num := amount / u.num + amount %= u.num + timeParts = append(timeParts, fmt.Sprintf("%d%s", num, u.name)) + } + } + return strings.Join(timeParts, " ") +} diff --git a/modules/util/time_str_test.go b/modules/util/time_str_test.go new file mode 100644 index 0000000000..67b7978d0b --- /dev/null +++ b/modules/util/time_str_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 Gitea. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTimeStr(t *testing.T) { + t.Run("Parse", func(t *testing.T) { + // Test TimeEstimateParse + tests := []struct { + input string + output int64 + err bool + }{ + {"1h", 3600, false}, + {"1m", 60, false}, + {"1s", 1, false}, + {"1h 1m 1s", 3600 + 60 + 1, false}, + {"1d1x", 0, true}, + } + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + output, err := TimeEstimateParse(test.input) + if test.err { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + assert.Equal(t, test.output, output) + }) + } + }) + t.Run("String", func(t *testing.T) { + tests := []struct { + input int64 + output string + }{ + {3600, "1h"}, + {60, "1m"}, + {1, "1s"}, + {3600 + 1, "1h 1s"}, + } + for _, test := range tests { + t.Run(test.output, func(t *testing.T) { + output := TimeEstimateString(test.input) + assert.Equal(t, test.output, output) + }) + } + }) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 395063faf8..e5c3cd38c8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1670,27 +1670,34 @@ issues.comment_on_locked = You cannot comment on a locked issue. issues.delete = Delete issues.delete.title = Delete this issue? issues.delete.text = Do you really want to delete this issue? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) + issues.tracker = Time Tracker -issues.start_tracking_short = Start Timer -issues.start_tracking = Start Time Tracking -issues.start_tracking_history = `started working %s` +issues.timetracker_timer_start = Start timer +issues.timetracker_timer_stop = Stop timer +issues.timetracker_timer_discard = Discard timer +issues.timetracker_timer_manually_add = Add Time + +issues.time_estimate_placeholder = 1h 2m +issues.time_estimate_set = Set estimated time +issues.time_estimate_display = Estimate: %s +issues.change_time_estimate_at = changed time estimate to %s %s +issues.remove_time_estimate_at = removed time estimate %s +issues.time_estimate_invalid = Time estimate format is invalid +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.cancel_tracking = Discard +issues.stop_tracking_history = worked for %s %s 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.add_time_history = added spent time %s %s issues.del_time_history= `deleted spent time %s` +issues.add_time_manually = Manually Add Time 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.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." diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index 70d42b27c0..5d6e723311 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -4,7 +4,6 @@ package repo import ( - "net/http" "strings" "code.gitea.io/gitea/models/db" @@ -40,8 +39,7 @@ func IssueStopwatch(c *context.Context) { c.Flash.Success(c.Tr("repo.issues.tracker_auto_close")) } - url := issue.Link() - c.Redirect(url, http.StatusSeeOther) + c.JSONRedirect("") } // CancelStopwatch cancel the stopwatch @@ -72,8 +70,7 @@ func CancelStopwatch(c *context.Context) { }) } - url := issue.Link() - c.Redirect(url, http.StatusSeeOther) + c.JSONRedirect("") } // GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 099711c5e9..88c539488e 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -5,6 +5,7 @@ package repo import ( "net/http" + "strings" "time" "code.gitea.io/gitea/models/db" @@ -13,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" + issue_service "code.gitea.io/gitea/services/issue" ) // AddTimeManually tracks time manually @@ -26,19 +28,16 @@ func AddTimeManually(c *context.Context) { c.NotFound("CanUseTimetracker", nil) return } - url := issue.Link() if c.HasError() { - c.Flash.Error(c.GetErrMsg()) - c.Redirect(url) + c.JSONError(c.GetErrMsg()) return } total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute if total <= 0 { - c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small")) - c.Redirect(url, http.StatusSeeOther) + c.JSONError(c.Tr("repo.issues.add_time_sum_to_small")) return } @@ -47,7 +46,7 @@ func AddTimeManually(c *context.Context) { return } - c.Redirect(url, http.StatusSeeOther) + c.JSONRedirect("") } // DeleteTime deletes tracked time @@ -83,5 +82,38 @@ func DeleteTime(c *context.Context) { } c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) - c.Redirect(issue.Link()) + c.JSONRedirect("") +} + +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 + } + + timeStr := strings.TrimSpace(ctx.FormString("time_estimate")) + + total, err := util.TimeEstimateParse(timeStr) + if err != nil { + ctx.JSONError(ctx.Tr("repo.issues.time_estimate_invalid")) + return + } + + // No time changed + if issue.TimeEstimate == total { + ctx.JSONRedirect("") + return + } + + if err := issue_service.ChangeTimeEstimate(ctx, issue, ctx.Doer, total); err != nil { + ctx.ServerError("ChangeTimeEstimate", err) + return + } + + ctx.JSONRedirect("") } diff --git a/routers/web/web.go b/routers/web/web.go index e89d069449..6113a6457b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1235,6 +1235,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 9ec9ac7684..b8527ae233 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.TimeEstimateString(timeSec) + } } comment := &api.TimelineComment{ diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index b9677c1800..76382ddfdd 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/issue.go b/services/issue/issue.go index 72ea66c8d9..c6a52cc0fe 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/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl index 9a49664b0e..1acf56d7b2 100644 --- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl +++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl @@ -1,60 +1,78 @@ {{if .Repository.IsTimetrackerEnabled ctx}} {{if and .CanUseTimetracker (not .Repository.IsArchived)}}
-
- {{ctx.Locale.Tr "repo.issues.tracker"}} -
-
- {{$.CsrfTokenHtml}} -
-
- {{$.CsrfTokenHtml}} -
- {{if $.IsStopwatchRunning}} - - - {{else}} - {{if .HasUserStopwatch}} -
- {{ctx.Locale.Tr "repo.issues.tracking_already_started" .OtherStopwatchURL}} -
+
+ {{end}} {{if .WorkingUsers}} -
-
- {{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}} +
+ {{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}
{{range $user, $trackedtime := .WorkingUsers}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 47551c86e4..3f9af18027 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,11 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}} + {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} + {{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}} + {{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
{{else if eq .Type 14}}
@@ -269,18 +263,11 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}} + {{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}} + {{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}} + {{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr}} {{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}} -
- {{svg "octicon-clock"}} - {{if .RenderedContent}} - {{/* compatibility with time comments made before v1.21 */}} - {{.RenderedContent}} - {{else}} - {{.Content|Sec2Time}} - {{end}} -
{{else if eq .Type 15}}
@@ -703,6 +690,20 @@ {{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}} + {{$timeStr := .Content|TimeEstimateString}} + {{if $timeStr}} + {{ctx.Locale.Tr "repo.issues.change_time_estimate_at" $timeStr $createdStr}} + {{else}} + {{ctx.Locale.Tr "repo.issues.remove_time_estimate_at" $createdStr}} + {{end}} + +
{{end}} {{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments_delete_time.tmpl b/templates/repo/issue/view_content/comments_delete_time.tmpl index 2377e7c4f0..5534a30df8 100644 --- a/templates/repo/issue/view_content/comments_delete_time.tmpl +++ b/templates/repo/issue/view_content/comments_delete_time.tmpl @@ -2,14 +2,10 @@ {{if (not .comment.Time.Deleted)}} {{if (or .ctxData.IsAdmin (and .ctxData.IsSigned (eq .ctxData.SignedUserID .comment.PosterID)))}} - - diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index 933bb51cf8..2217ddec2e 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -24,15 +24,9 @@ func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc { return &HTMLDoc{doc: doc} } -// GetInputValueByID for get input value by id -func (doc *HTMLDoc) GetInputValueByID(id string) string { - text, _ := doc.doc.Find("#" + id).Attr("value") - return text -} - // GetInputValueByName for get input value by name func (doc *HTMLDoc) GetInputValueByName(name string) string { - text, _ := doc.doc.Find("input[name=\"" + name + "\"]").Attr("value") + text, _ := doc.doc.Find(`input[name="` + name + `"]`).Attr("value") return text } diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go index 10e539cbe6..d459de5df6 100644 --- a/tests/integration/timetracking_test.go +++ b/tests/integration/timetracking_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -17,22 +16,24 @@ import ( func TestViewTimetrackingControls(t *testing.T) { defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - testViewTimetrackingControls(t, session, "user2", "repo1", "1", true) - // user2/repo1 -} -func TestNotViewTimetrackingControls(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user5") - testViewTimetrackingControls(t, session, "user2", "repo1", "1", false) - // user2/repo1 -} + t.Run("Exist", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + session := loginUser(t, "user2") + testViewTimetrackingControls(t, session, "user2", "repo1", "1", true) + }) -func TestViewTimetrackingControlsDisabled(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - testViewTimetrackingControls(t, session, "org3", "repo3", "1", false) + t.Run("Non-exist", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + session := loginUser(t, "user5") + testViewTimetrackingControls(t, session, "user2", "repo1", "1", false) + }) + + t.Run("Disabled", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + session := loginUser(t, "user2") + testViewTimetrackingControls(t, session, "org3", "repo3", "1", false) + }) } func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo, issue string, canTrackTime bool) { @@ -41,40 +42,40 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo htmlDoc := NewHTMLParser(t, resp.Body) - htmlDoc.AssertElement(t, ".timetrack .issue-start-time", canTrackTime) - htmlDoc.AssertElement(t, ".timetrack .issue-add-time", canTrackTime) + htmlDoc.AssertElement(t, ".issue-start-time", canTrackTime) + htmlDoc.AssertElement(t, ".issue-add-time", canTrackTime) - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{ + issueLink := path.Join(user, repo, "issues", issue) + req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{ "_csrf": htmlDoc.GetCSRF(), }) if canTrackTime { - resp = session.MakeRequest(t, req, http.StatusSeeOther) + session.MakeRequest(t, req, http.StatusOK) - req = NewRequest(t, "GET", test.RedirectURL(resp)) + req = NewRequest(t, "GET", issueLink) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc = NewHTMLParser(t, resp.Body) events := htmlDoc.doc.Find(".event > span.text") assert.Contains(t, events.Last().Text(), "started working") - htmlDoc.AssertElement(t, ".timetrack .issue-stop-time", true) - htmlDoc.AssertElement(t, ".timetrack .issue-cancel-time", true) + htmlDoc.AssertElement(t, ".issue-stop-time", true) + htmlDoc.AssertElement(t, ".issue-cancel-time", true) // Sleep for 1 second to not get wrong order for stopping timer time.Sleep(time.Second) - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "issues", issue, "times", "stopwatch", "toggle"), map[string]string{ + req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{ "_csrf": htmlDoc.GetCSRF(), }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) + session.MakeRequest(t, req, http.StatusOK) - req = NewRequest(t, "GET", test.RedirectURL(resp)) + req = NewRequest(t, "GET", issueLink) resp = session.MakeRequest(t, req, http.StatusOK) 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) } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index a59e4319ab..477edbeb5f 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -11,37 +11,6 @@ import {initRepoIssueSidebar} from './repo-issue-sidebar.ts'; const {appSubUrl} = window.config; -export function initRepoIssueTimeTracking() { - $(document).on('click', '.issue-add-time', () => { - $('.issue-start-time-modal').modal({ - duration: 200, - onApprove() { - $('#add_time_manual_form').trigger('submit'); - }, - }).modal('show'); - $('.issue-start-time-modal input').on('keydown', (e) => { - if (e.key === 'Enter') { - $('#add_time_manual_form').trigger('submit'); - } - }); - }); - $(document).on('click', '.issue-start-time, .issue-stop-time', () => { - $('#toggle_stopwatch_form').trigger('submit'); - }); - $(document).on('click', '.issue-cancel-time', () => { - $('#cancel_stopwatch_form').trigger('submit'); - }); - $(document).on('click', 'button.issue-delete-time', function () { - const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`; - $(sel).modal({ - duration: 200, - onApprove() { - $(`${sel} form`).trigger('submit'); - }, - }).modal('show'); - }); -} - /** * @param {HTMLElement} item */ diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 48c4b76cee..f93c3495af 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -26,7 +26,6 @@ import {initPdfViewer} from './render/pdf.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; import { initRepoIssueReferenceRepositorySearch, - initRepoIssueTimeTracking, initRepoIssueWipTitle, initRepoPullRequestMergeInstruction, initRepoPullRequestAllowMaintainerEdit, @@ -184,7 +183,6 @@ onDomReady(() => { initRepoIssueList, initRepoIssueSidebarList, initRepoIssueReferenceRepositorySearch, - initRepoIssueTimeTracking, initRepoIssueWipTitle, initRepoMigration, initRepoMigrationStatusChecker,