This commit is contained in:
stuzer05 2023-02-24 17:22:59 +02:00
parent 1b7ba4189b
commit f7427d8b50
11 changed files with 230 additions and 121 deletions

View File

@ -6,9 +6,12 @@
package issues
import (
"code.gitea.io/gitea/modules/translation"
"context"
"fmt"
"math"
"strconv"
"strings"
"unicode/utf8"
"code.gitea.io/gitea/models/db"
@ -305,8 +308,7 @@ type Comment struct {
CommitsNum int64 `xorm:"-"`
IsForcePush bool `xorm:"-"`
TimeEstimateHours int
TimeEstimateMinutes int
TimeEstimate int64
}
func init() {
@ -793,40 +795,39 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
}
comment := &Comment{
Type: opts.Type,
PosterID: opts.Doer.ID,
Poster: opts.Doer,
IssueID: opts.Issue.ID,
LabelID: LabelID,
OldMilestoneID: opts.OldMilestoneID,
MilestoneID: opts.MilestoneID,
OldProjectID: opts.OldProjectID,
ProjectID: opts.ProjectID,
TimeID: opts.TimeID,
RemovedAssignee: opts.RemovedAssignee,
AssigneeID: opts.AssigneeID,
AssigneeTeamID: opts.AssigneeTeamID,
CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA,
Line: opts.LineNum,
Content: opts.Content,
OldTitle: opts.OldTitle,
NewTitle: opts.NewTitle,
OldRef: opts.OldRef,
NewRef: opts.NewRef,
DependentIssueID: opts.DependentIssueID,
TreePath: opts.TreePath,
ReviewID: opts.ReviewID,
Patch: opts.Patch,
RefRepoID: opts.RefRepoID,
RefIssueID: opts.RefIssueID,
RefCommentID: opts.RefCommentID,
RefAction: opts.RefAction,
RefIsPull: opts.RefIsPull,
IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated,
TimeEstimateHours: opts.TimeEstimateHours,
TimeEstimateMinutes: opts.TimeEstimateMinutes,
Type: opts.Type,
PosterID: opts.Doer.ID,
Poster: opts.Doer,
IssueID: opts.Issue.ID,
LabelID: LabelID,
OldMilestoneID: opts.OldMilestoneID,
MilestoneID: opts.MilestoneID,
OldProjectID: opts.OldProjectID,
ProjectID: opts.ProjectID,
TimeID: opts.TimeID,
RemovedAssignee: opts.RemovedAssignee,
AssigneeID: opts.AssigneeID,
AssigneeTeamID: opts.AssigneeTeamID,
CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA,
Line: opts.LineNum,
Content: opts.Content,
OldTitle: opts.OldTitle,
NewTitle: opts.NewTitle,
OldRef: opts.OldRef,
NewRef: opts.NewRef,
DependentIssueID: opts.DependentIssueID,
TreePath: opts.TreePath,
ReviewID: opts.ReviewID,
Patch: opts.Patch,
RefRepoID: opts.RefRepoID,
RefIssueID: opts.RefIssueID,
RefCommentID: opts.RefCommentID,
RefAction: opts.RefAction,
RefIsPull: opts.RefIsPull,
IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated,
TimeEstimate: opts.TimeEstimate,
}
if _, err = e.Insert(comment); err != nil {
return nil, err
@ -970,36 +971,35 @@ type CreateCommentOptions struct {
Issue *Issue
Label *Label
DependentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64
ProjectID int64
TimeID int64
AssigneeID int64
AssigneeTeamID int64
RemovedAssignee bool
OldTitle string
NewTitle string
OldRef string
NewRef string
CommitID int64
CommitSHA string
Patch string
LineNum int64
TreePath string
ReviewID int64
Content string
Attachments []string // UUIDs of attachments
RefRepoID int64
RefIssueID int64
RefCommentID int64
RefAction references.XRefAction
RefIsPull bool
IsForcePush bool
Invalidated bool
TimeEstimateHours int
TimeEstimateMinutes int
DependentIssueID int64
OldMilestoneID int64
MilestoneID int64
OldProjectID int64
ProjectID int64
TimeID int64
AssigneeID int64
AssigneeTeamID int64
RemovedAssignee bool
OldTitle string
NewTitle string
OldRef string
NewRef string
CommitID int64
CommitSHA string
Patch string
LineNum int64
TreePath string
ReviewID int64
Content string
Attachments []string // UUIDs of attachments
RefRepoID int64
RefIssueID int64
RefCommentID int64
RefAction references.XRefAction
RefIsPull bool
IsForcePush bool
Invalidated bool
TimeEstimate int64
}
// GetCommentByID returns the comment by given ID.
@ -1259,3 +1259,40 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
func (c *Comment) HasOriginalAuthor() bool {
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
}
// TimeEstimateStr returns formatted time estimate string from seconds (e.g. "2 weeks 4 days 12 hours 5 minutes")
func (c *Comment) TimeEstimateToStrTranslated(lang translation.Locale) string {
var timeParts []string
timeSeconds := float64(c.TimeEstimate)
// Format weeks
weeks := math.Floor(timeSeconds / (60 * 60 * 24 * 7))
if weeks > 0 {
timeParts = append(timeParts, lang.Tr("tool.hours", int64(weeks)))
}
timeSeconds -= weeks * (60 * 60 * 24 * 7)
// Format days
days := math.Floor(timeSeconds / (60 * 60 * 24))
if days > 0 {
timeParts = append(timeParts, lang.Tr("tool.days", int64(days)))
}
timeSeconds -= days * (60 * 60 * 24)
// Format hours
hours := math.Floor(timeSeconds / (60 * 60))
if hours > 0 {
timeParts = append(timeParts, lang.Tr("tool.hours", int64(hours)))
}
timeSeconds -= hours * (60 * 60)
// Format minutes
minutes := math.Floor(timeSeconds / (60))
if minutes > 0 {
timeParts = append(timeParts, lang.Tr("tool.minutes", int64(minutes)))
}
timeSeconds -= minutes * (60)
return strings.Join(timeParts[:], " ")
}

View File

@ -7,8 +7,10 @@ package issues
import (
"context"
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
@ -148,8 +150,7 @@ type Issue struct {
ShowRole RoleDescriptor `xorm:"-"`
// Time estimate
TimeEstimateHours int
TimeEstimateMinutes int
TimeEstimate int64
}
var (
@ -779,14 +780,14 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err
}
// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
func ChangeIssueTimeEstimate(issue *Issue, doer *user_model.User, timeEstimateHours, timeEstimateMinutes int) (err error) {
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, TimeEstimateHours: timeEstimateHours, TimeEstimateMinutes: timeEstimateMinutes}, "time_estimate_hours", "time_estimate_minutes"); err != nil {
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
return fmt.Errorf("updateIssueCols: %w", err)
}
@ -795,12 +796,12 @@ func ChangeIssueTimeEstimate(issue *Issue, doer *user_model.User, timeEstimateHo
}
opts := &CreateCommentOptions{
Type: CommentTypeChangeTimeEstimate,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
TimeEstimateHours: timeEstimateHours,
TimeEstimateMinutes: timeEstimateMinutes,
Type: CommentTypeChangeTimeEstimate,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
Content: util.SecToTime(timeEstimate),
TimeEstimate: timeEstimate,
}
if _, err = CreateComment(ctx, opts); err != nil {
return fmt.Errorf("createComment: %w", err)
@ -2472,3 +2473,81 @@ func DeleteOrphanedIssues(ctx context.Context) error {
func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
}
// TimeEstimateFromStr returns time estimate in seconds from formatted string
func (issue *Issue) TimeEstimateFromStr(timeStr string) int64 {
timeTotal := 0
// Time match regex
rWeeks, _ := regexp.Compile("([\\d]+)w")
rDays, _ := regexp.Compile("([\\d]+)d")
rHours, _ := regexp.Compile("([\\d]+)h")
rMinutes, _ := regexp.Compile("([\\d]+)m")
// Find time weeks
timeStrMatches := rWeeks.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60 * 24 * 7)
}
// Find time days
timeStrMatches = rDays.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60 * 24)
}
// Find time hours
timeStrMatches = rHours.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60)
}
// Find time minutes
timeStrMatches = rMinutes.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 (issue *Issue) TimeEstimateToStr() string {
var timeParts []string
timeSeconds := float64(issue.TimeEstimate)
// 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)))
}
timeSeconds -= minutes * (60)
return strings.Join(timeParts[:], " ")
}

View File

@ -41,21 +41,20 @@ type RepositoryMeta struct {
// Issue represents an issue in a repository
// swagger:model
type Issue struct {
ID int64 `json:"id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
Index int64 `json:"number"`
Poster *User `json:"user"`
OriginalAuthor string `json:"original_author"`
OriginalAuthorID int64 `json:"original_author_id"`
Title string `json:"title"`
Body string `json:"body"`
Ref string `json:"ref"`
Attachments []*Attachment `json:"assets"`
Labels []*Label `json:"labels"`
Milestone *Milestone `json:"milestone"`
TimeEstimateHours int64 `json:"time_estimate_hours"`
TimeEstimateMinutes int64 `json:"time_estimate_minutes"`
ID int64 `json:"id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
Index int64 `json:"number"`
Poster *User `json:"user"`
OriginalAuthor string `json:"original_author"`
OriginalAuthorID int64 `json:"original_author_id"`
Title string `json:"title"`
Body string `json:"body"`
Ref string `json:"ref"`
Attachments []*Attachment `json:"assets"`
Labels []*Label `json:"labels"`
Milestone *Milestone `json:"milestone"`
TimeEstimate int `json:"time_estimate"`
// deprecated
Assignee *User `json:"assignee"`
Assignees []*User `json:"assignees"`
@ -108,10 +107,9 @@ type EditIssueOption struct {
Milestone *int64 `json:"milestone"`
State *string `json:"state"`
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
RemoveDeadline *bool `json:"unset_due_date"`
TimeEstimateHours int `json:"time_estimate_hours"`
TimeEstimateMinutes int `json:"time_estimate_minutes"`
Deadline *time.Time `json:"due_date"`
RemoveDeadline *bool `json:"unset_due_date"`
TimeEstimate *string `json:"time_estimate"`
}
// EditDeadlineOption options for creating a deadline

View File

@ -1302,7 +1302,8 @@ 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.change_time_estimate_at = `changed time estimate to <b>%d hour %d minutes</b> %s`
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.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`

View File

@ -1982,17 +1982,16 @@ func UpdateIssueTimeEstimate(ctx *context.Context) {
return
}
timeEstimateHours := ctx.FormInt("time_estimate_hours")
timeEstimateMinutes := ctx.FormInt("time_estimate_minutes")
total := issue.TimeEstimateFromStr(ctx.FormString("time_estimate"))
if issue.TimeEstimateHours == timeEstimateHours && issue.TimeEstimateMinutes == timeEstimateMinutes {
if issue.TimeEstimate == total {
ctx.JSON(http.StatusOK, map[string]interface{}{
"status": "ok",
})
return
}
if err := issue_service.ChangeTimeEstimate(issue, ctx.Doer, timeEstimateHours, timeEstimateMinutes); err != nil {
if err := issue_service.ChangeTimeEstimate(issue, ctx.Doer, total); err != nil {
ctx.ServerError("ChangeTimeEstimate", err)
return
}

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 := issue.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.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
if _, err := issues_model.AddTime(c.Doer, issue, total, time.Now()); err != nil {
c.ServerError("AddTime", err)
return
}

View File

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

@ -61,12 +61,11 @@ func ChangeTitle(issue *issues_model.Issue, doer *user_model.User, title string)
return nil
}
// ChangeTitle changes the title of this issue, as the given user.
func ChangeTimeEstimate(issue *issues_model.Issue, doer *user_model.User, timeEstimateHours, timeEstimateMinutes int) (err error) {
issue.TimeEstimateHours = timeEstimateHours
issue.TimeEstimateMinutes = timeEstimateMinutes
// 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
if err = issues_model.ChangeIssueTimeEstimate(issue, doer, timeEstimateHours, timeEstimateMinutes); err != nil {
if err = issues_model.ChangeIssueTimeEstimate(issue, doer, timeEstimate); err != nil {
return
}

View File

@ -799,10 +799,11 @@
{{template "shared/user/avatarlink" Dict "Context" $.Context "user" .Poster}}
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{if and (eq .TimeEstimateHours 0) (eq .TimeEstimateMinutes 0)}}
{{if and (eq .TimeEstimate 0)}}
{{$.locale.Tr "repo.issues.remove_time_estimate" $createdStr | Safe}}
{{else}}
{{$.locale.Tr "repo.issues.change_time_estimate_at" (.TimeEstimateHours) (.TimeEstimateMinutes) $createdStr | Safe}}
{{$timeStr := .TimeEstimateToStrTranslated $.locale}}
{{$.locale.Tr "repo.issues.change_time_estimate_at" $timeStr $createdStr | Safe}}
{{end}}
</span>
</div>

View File

@ -364,9 +364,8 @@
<form method="POST" id="set_time_estimate_form" class="gt-mt-3" action="{{.Issue.Link}}/time_estimate">
{{$.CsrfTokenHtml}}
<div class="ui action input fluid">
<input placeholder='{{.locale.Tr "repo.issues.add_time_hours"}}' type="number" min="0" value="{{ ($.Issue.TimeEstimateHours) }}" name="time_estimate_hours">
<input placeholder='{{.locale.Tr "repo.issues.add_time_minutes"}}' type="number" min="0" max="59" value="{{ ($.Issue.TimeEstimateMinutes) }}" name="time_estimate_minutes" class="ui compact">
<div class="ui input fluid">
<input name="time_estimate" placeholder='{{.locale.Tr "repo.issues.add_time_estimate"}}' value="{{ ($.Issue.TimeEstimateToStr) }}" type="text" >
</div>
<button class="ui fluid button green tooltip gt-mt-3">
{{.locale.Tr "repo.issues.save"}}
@ -396,10 +395,9 @@
<div class="ui mini modal issue-start-time-modal">
<div class="header">{{.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 action input fluid">
<form method="POST" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid">
{{$.CsrfTokenHtml}}
<input placeholder='{{.locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
<input placeholder='{{.locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
<input placeholder='{{.locale.Tr "repo.issues.add_time_estimate"}}' type="text" name="time_string">
</form>
</div>
<div class="actions">

View File

@ -643,13 +643,11 @@ export function initRepoIssueTimeEstimateEdit() {
$('#set_time_estimate_form').on('submit', function(e) {
e.preventDefault();
const timeEstimateHours = $(this).find('[name=time_estimate_hours]').val();
const timeEstimateMinutes = $(this).find('[name=time_estimate_minutes]').val();
const timeEstimate = $(this).find('[name=time_estimate]').val();
$.post($(this).attr('action'), {
_csrf: csrfToken,
time_estimate_hours: timeEstimateHours,
time_estimate_minutes: timeEstimateMinutes,
time_estimate: timeEstimate,
}).always(() => {
window.location.reload();
});