Compare commits

...

95 Commits

Author SHA1 Message Date
Illya Marchenko 46d980a0e1
Merge a71b4578e8 into 22c7b3a744 2024-05-06 09:45:21 +08:00
Kemal Zebari 22c7b3a744
Have time.js use UTC-related getters/setters (#30857)
Before this patch, we were using `Date` getter/setter methods that
worked with local time to get a list of Sundays that are in the range of
some start date and end date. The problem with this was that the Sundays
are in Unix epoch time and when we changed the "startDate" argument that
was passed to make sure it is on a Sunday, this change would be
reflected when we convert it to Unix epoch time. More specifically, I
observed that we may get different Unix epochs depending on your
timezone when the returned list should rather be timezone-agnostic.

This led to issues in US timezones that caused the contributor, code
frequency, and recent commit charts to not show any chart data. This fix
resolves this by using getter/setter methods that work with UTC since it
isn't dependent on timezones.

Fixes #30851.

---------

Co-authored-by: Sam Fisher <fisher@3echelon.local>
2024-05-06 09:36:53 +08:00
wxiaoguang 982b20d259
Do not show monaco JS errors (#30862)
Fix #30861
2024-05-05 16:34:13 +00:00
stuzer05 a71b4578e8
Commit 2024-05-02 14:27:45 +03:00
stuzer05 0c44cf7512
Commit 2024-04-30 18:02:08 +03:00
stuzer05 8c92f4675c
Merge branch 'main' into add-issue-planned-time 2024-04-30 17:56:19 +03:00
stuzer05 0a8fd353ef
Merge branch 'main' into add-issue-planned-time 2024-04-25 21:51:46 +03:00
stuzer05 12553d7dc2
Merge branch 'main' into add-issue-planned-time 2024-04-22 12:16:56 +03:00
stuzer05 b97139b232
Merge branch 'main' into add-issue-planned-time 2024-04-15 09:28:34 +03:00
stuzer05 b452e13f35
Commit 2024-04-13 15:38:12 +03:00
stuzer05 e92bedc81d
Commit 2024-04-13 15:04:15 +03:00
stuzer05 1e22cc242f
Merge branch 'main' into add-issue-planned-time 2024-04-13 14:17:17 +03:00
stuzer05 93d09fa8fc
Merge branch 'main' into add-issue-planned-time 2024-03-29 10:38:46 +02:00
stuzer05 0820db04ed
Merge branch 'main' into add-issue-planned-time 2024-03-28 11:18:38 +02:00
stuzer05 09723c5e0b
Merge branch 'main' into add-issue-planned-time 2024-03-17 15:52:06 +02:00
stuzer05 c272512d9a
Merge branch 'main' into add-issue-planned-time 2024-03-04 12:07:39 +02:00
stuzer05 0c4b2dfa8f
Commit 2024-03-04 11:59:57 +02:00
stuzer05 805af19ef1
Merge branch 'main' into add-issue-planned-time 2024-03-04 11:59:19 +02:00
stuzer05 bb5ca4c40b
Merge branch 'main' into add-issue-planned-time 2024-02-28 13:50:55 +02:00
stuzer05 a737a8c10b
Merge branch 'main' into add-issue-planned-time 2024-02-19 09:52:21 +02:00
stuzer05 92dc2cd22c
Merge branch 'main' into add-issue-planned-time 2024-02-11 12:12:41 +02:00
stuzer05 39b8b1929d
Merge branch 'main' into add-issue-planned-time 2024-01-22 09:31:58 +02:00
stuzer05 a999055954
Merge branch 'main' into add-issue-planned-time 2024-01-17 10:37:05 +02:00
stuzer05 a6fa4c3d04
Merge branch 'main' into add-issue-planned-time 2024-01-15 11:47:48 +02:00
stuzer05 2a9009fb02
Merge branch 'main' into add-issue-planned-time 2023-10-16 12:02:04 +03:00
stuzer05 3037d6cdde
Merge branch 'main' into add-issue-planned-time 2023-10-08 20:48:33 +03:00
stuzer05 349b959022
Merge branch 'main' into add-issue-planned-time 2023-10-07 15:34:04 +03:00
stuzer05 64de74d540
Merge branch 'main' into add-issue-planned-time 2023-09-11 10:55:43 +03:00
stuzer05 fa662ec087
Merge branch 'main' into add-issue-planned-time 2023-07-10 10:07:08 +03:00
stuzer05 62094d8cf3
Merge branch 'main' into add-issue-planned-time 2023-06-26 16:12:25 +03:00
スツゼル e933a89bd9
Delete serviceworker.js 2023-06-24 09:36:32 +03:00
stuzer05 db49783fa8
Commit 2023-06-24 09:35:31 +03:00
stuzer05 a45c1e9c86
Merge branch 'main' into add-issue-planned-time 2023-06-24 09:24:08 +03:00
stuzer05 721069d766
Merge branch 'main' into add-issue-planned-time 2023-06-23 10:49:56 +03:00
stuzer05 3310440ed1
Hide time tracking 2023-06-23 10:34:52 +03:00
スツゼル 598e2d5bed
Merge branch 'main' into add-issue-planned-time 2023-06-19 16:34:14 +03:00
silverwind b211b9e66d
Merge branch 'main' into add-issue-planned-time 2023-06-18 20:43:42 +02:00
stuzer05 ffaa4babcb
Format 2023-06-18 19:35:49 +03:00
stuzer05 cef496af2a
Remove unused code 2023-06-18 19:34:22 +03:00
stuzer05 015ad01513
Merge branch 'main' into add-issue-planned-time 2023-06-18 17:27:33 +03:00
stuzer05 748bd67814
Fix displaying issue estimation 2023-06-14 11:31:49 +03:00
スツゼル 3924cb0062
Merge branch 'go-gitea:main' into add-issue-planned-time 2023-06-11 20:02:21 +03:00
stuzer05 e9afd60d6b
Refactor helper functions 2023-06-11 20:01:28 +03:00
stuzer05 40e1373dce
Merge branch 'main' into add-issue-planned-time 2023-06-06 11:12:26 +03:00
stuzer05 bf4fa112d0
Merge branch 'main' into add-issue-planned-time 2023-06-03 19:28:20 +03:00
stuzer05 fc93006f50
Merge branch 'main' into add-issue-planned-time 2023-04-18 07:16:00 +03:00
スツゼル 879d96f077
Merge branch 'main' into add-issue-planned-time 2023-03-04 23:04:29 +02:00
スツゼル fd5adc55f3
Merge branch 'main' into add-issue-planned-time 2023-02-28 18:10:11 +02:00
stuzer05 57a3664eb2
Commit 2023-02-28 18:06:04 +02:00
stuzer05 bf323cf26c
Commit 2023-02-28 16:07:09 +02:00
スツゼル fb8126035a
Update models/issues/issue.go
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
2023-02-28 16:04:21 +02:00
stuzer05 deddce59cf
Commit 2023-02-28 15:17:00 +02:00
stuzer05 79e18c6711
Commit 2023-02-28 10:11:49 +02:00
stuzer05 49a176d37c
Commit 2023-02-28 10:02:08 +02:00
stuzer05 8ef3a478aa
Commit 2023-02-27 17:40:07 +02:00
stuzer05 37e8e8ddaf
Commit 2023-02-27 16:49:29 +02:00
stuzer05 e20e23b60a
Commit 2023-02-27 16:17:58 +02:00
スツゼル 8c0bf885b2
Merge branch 'main' into add-issue-planned-time 2023-02-27 13:51:04 +02:00
stuzer05 b9cdc7c670 Commit 2023-02-26 11:51:37 +02:00
stuzer05 7be748f63b Commit 2023-02-24 23:54:01 +02:00
stuzer05 29dd61722b Commit 2023-02-24 23:33:27 +02:00
stuzer05 875087061f Commit 2023-02-24 23:28:13 +02:00
スツゼル 775c663805
Merge branch 'main' into add-issue-planned-time 2023-02-24 23:08:56 +02:00
stuzer05 db3c697178 Commit 2023-02-24 23:07:48 +02:00
stuzer05 5eea230714 Commit 2023-02-24 23:06:41 +02:00
stuzer05 0ab85af1ce Commit 2023-02-24 22:26:42 +02:00
stuzer05 5c4dc8739c Commit 2023-02-24 20:13:46 +02:00
stuzer05 79f507b81f Commit 2023-02-24 20:07:44 +02:00
stuzer05 7a570440f1 Commit 2023-02-24 19:44:24 +02:00
stuzer05 f463765fec Commit 2023-02-24 19:31:40 +02:00
stuzer05 e15549501d Commit 2023-02-24 19:29:51 +02:00
stuzer05 1cff1a9e05 Commit 2023-02-24 19:06:43 +02:00
stuzer05 f7427d8b50 Commit 2023-02-24 17:22:59 +02:00
stuzer05 1b7ba4189b Commit 2023-02-24 14:14:13 +02:00
stuzer05 11b9719b5f Commit 2023-02-24 14:13:27 +02:00
stuzer05 0f5b609f9d Commit 2023-02-24 14:13:03 +02:00
stuzer05 09c05e8b76 Commit 2023-02-24 14:12:12 +02:00
stuzer05 d247b0ffd1 Commit 2023-02-24 14:08:38 +02:00
stuzer05 870bb922cc Commit 2023-02-24 14:05:26 +02:00
stuzer05 b062fc9849 Commit 2023-02-24 14:03:03 +02:00
stuzer05 2fc2f63c6c Commit 2023-02-24 14:02:36 +02:00
スツゼル 4e1aed8a61
Merge branch 'main' into add-issue-planned-time 2023-02-24 13:58:30 +02:00
stuzer05 f33b0a0773 Commit 2023-02-24 13:57:46 +02:00
stuzer05 e187364d7a Commit 2023-02-24 13:53:53 +02:00
スツゼル f7a4c9e0aa
Update services/issue/issue.go
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
2023-02-24 13:39:32 +02:00
スツゼル 5f3edad64f
Update services/issue/issue.go
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
2023-02-24 13:38:35 +02:00
stuzer05 4be8c50a6f Commit 2023-02-24 13:38:05 +02:00
stuzer05 d75b7acac6 Commit 2023-02-24 13:36:53 +02:00
stuzer05 c248042c8e Commit 2023-02-24 13:35:16 +02:00
stuzer05 7ed86ff00a Commit 2023-02-24 12:41:38 +02:00
stuzer05 d9446629c3 Commit 2023-02-24 12:37:12 +02:00
stuzer05 a8778f4db7 Commit 2023-02-24 12:15:43 +02:00
stuzer05 c783692f6c Commit 2023-02-24 12:13:12 +02:00
stuzer05 d11ba9f46c Commit 2023-02-24 12:10:52 +02:00
stuzer05 1363205f29 Commit 2023-02-24 12:06:12 +02:00
25 changed files with 378 additions and 129 deletions

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

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

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_22"
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@ -573,7 +574,7 @@ var migrations = []Migration{
// v293 -> v294
NewMigration("Ensure every project has exactly one default column", v1_22.CheckProjectColumnsConsistency),
// Gitea 1.22.0-rc0 ends at 294
// Gitea 1.22.0 ends at 294
// v294 -> v295
NewMigration("Add unique index for project issue table", v1_22.AddUniqueIndexForProjectIssue),
@ -587,6 +588,9 @@ var migrations = []Migration{
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// Gitea 1.22.0-rc1 ends at 299
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
// v300 -> v301
NewMigration("Add TimeEstimate to issue table", v1_23.AddTimeEstimateColumnToIssueTable),
}
// GetCurrentDBVersion returns the current db version

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

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

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
modules/util/time_str.go Normal file
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, " ")
}

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

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 {
@ -2211,6 +2215,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)

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
}

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)

View File

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

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

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,

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
}

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

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>

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

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

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

View File

@ -6,13 +6,20 @@
// This file must be imported before any lazy-loading is being attempted.
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
export function showGlobalErrorMessage(msg) {
const pageContent = document.querySelector('.page-content');
if (!pageContent) return;
function shouldIgnoreError(err) {
const ignorePatterns = [
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
];
for (const pattern of ignorePatterns) {
if (err.stack?.includes(pattern)) return true;
}
return false;
}
// compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = msg.replace(/\W/g, '').trim();
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
export function showGlobalErrorMessage(msg) {
const msgContainer = document.querySelector('.page-content') ?? document.body;
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
if (!msgDiv) {
const el = document.createElement('div');
el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`;
@ -23,7 +30,7 @@ export function showGlobalErrorMessage(msg) {
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact);
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString());
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : '');
pageContent.prepend(msgDiv);
msgContainer.prepend(msgDiv);
}
/**
@ -52,10 +59,12 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno
if (runModeIsProd) return;
}
// If the error stack trace does not include the base URL of our script assets, it likely came
// from a browser extension or inline script. Do not show such errors in production.
if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) {
return;
if (err instanceof Error) {
// If the error stack trace does not include the base URL of our script assets, it likely came
// from a browser extension or inline script. Do not show such errors in production.
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return;
// Ignore some known errors that are unable to fix
if (shouldIgnoreError(err)) return;
}
let msg = err?.message ?? message;

View File

@ -67,7 +67,7 @@ export default {
const weekValues = Object.values(this.data);
const start = weekValues[0].week;
const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(start), new Date(end));
const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
this.errorText = '';
} else {

View File

@ -114,7 +114,7 @@ export default {
const weekValues = Object.values(total.weeks);
this.xAxisStart = weekValues[0].week;
this.xAxisEnd = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd);
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
this.xAxisMin = this.xAxisStart;
this.xAxisMax = this.xAxisEnd;

View File

@ -62,7 +62,7 @@ export default {
const data = await response.json();
const start = Object.values(data)[0].week;
const end = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(new Date(start), new Date(end));
const startDays = startDaysBetween(start, end);
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52);
this.errorText = '';
} else {

View File

@ -1,25 +1,30 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import {getCurrentLocale} from '../utils.js';
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
export function startDaysBetween(startDate, endDate) {
// Ensure the start date is a Sunday
while (startDate.getDay() !== 0) {
startDate.setDate(startDate.getDate() + 1);
}
dayjs.extend(utc);
const start = dayjs(startDate);
const end = dayjs(endDate);
const startDays = [];
/**
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
*
* @param startConfig The start date. Can take any type that `Date` accepts.
* @param endConfig The end date. Can take any type that `Date` accepts.
*/
export function startDaysBetween(startDate, endDate) {
const start = dayjs.utc(startDate);
const end = dayjs.utc(endDate);
let current = start;
// Ensure the start date is a Sunday
while (current.day() !== 0) {
current = current.add(1, 'day');
}
const startDays = [];
while (current.isBefore(end)) {
startDays.push(current.valueOf());
// we are adding 7 * 24 hours instead of 1 week because we don't want
// date library to use local time zone to calculate 1 week from now.
// local time zone is problematic because of daylight saving time (dst)
// used on some countries
current = current.add(7 * 24, 'hour');
current = current.add(1, 'week');
}
return startDays;
@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) {
if (!(inputDate instanceof Date)) {
throw new Error('Invalid date');
}
const dayOfWeek = inputDate.getDay();
const dayOfWeek = inputDate.getUTCDay();
const daysUntilSunday = 7 - dayOfWeek;
const resultDate = new Date(inputDate.getTime());
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
return resultDate.valueOf();
}