Fix milestone deadline and date related problems (#32339)

Use zero instead of 9999-12-31 for deadline
Fix #32291

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
Lunny Xiao 2024-11-04 23:46:40 -08:00 committed by GitHub
parent 1887c75c35
commit 24b83ff63e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 147 additions and 165 deletions

View File

@ -7,19 +7,17 @@ import (
"testing" "testing"
"time" "time"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestActionScheduleSpec_Parse(t *testing.T) { func TestActionScheduleSpec_Parse(t *testing.T) {
// Mock the local timezone is not UTC // Mock the local timezone is not UTC
local := time.Local
tz, err := time.LoadLocation("Asia/Shanghai") tz, err := time.LoadLocation("Asia/Shanghai")
require.NoError(t, err) require.NoError(t, err)
defer func() { defer test.MockVariableValue(&time.Local, tz)()
time.Local = local
}()
time.Local = tz
now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00")
require.NoError(t, err) require.NoError(t, err)

View File

@ -84,10 +84,9 @@ func (m *Milestone) BeforeUpdate() {
// this object. // this object.
func (m *Milestone) AfterLoad() { func (m *Milestone) AfterLoad() {
m.NumOpenIssues = m.NumIssues - m.NumClosedIssues m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
if m.DeadlineUnix.Year() == 9999 { if m.DeadlineUnix == 0 {
return return
} }
m.DeadlineString = m.DeadlineUnix.FormatDate() m.DeadlineString = m.DeadlineUnix.FormatDate()
if m.IsClosed { if m.IsClosed {
m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix

View File

@ -364,6 +364,7 @@ func prepareMigrationTasks() []*migration {
newMigration(304, "Add index for release sha1", v1_23.AddIndexForReleaseSha1), newMigration(304, "Add index for release sha1", v1_23.AddIndexForReleaseSha1),
newMigration(305, "Add Repository Licenses", v1_23.AddRepositoryLicenses), newMigration(305, "Add Repository Licenses", v1_23.AddRepositoryLicenses),
newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection), newMigration(306, "Add BlockAdminMergeOverride to ProtectedBranch", v1_23.AddBlockAdminMergeOverrideBranchProtection),
newMigration(307, "Fix milestone deadline_unix when there is no due date", v1_23.FixMilestoneNoDueDate),
} }
return preparedMigrations return preparedMigrations
} }

View File

@ -0,0 +1,21 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_23 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
)
func FixMilestoneNoDueDate(x *xorm.Engine) error {
type Milestone struct {
DeadlineUnix timeutil.TimeStamp
}
// Wednesday, December 1, 9999 12:00:00 AM GMT+00:00
_, err := x.Table("milestone").Where("deadline_unix > 253399622400").
Cols("deadline_unix").
Update(&Milestone{DeadlineUnix: 0})
return err
}

View File

@ -32,7 +32,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo
graphCmd.AddArguments("--all") graphCmd.AddArguments("--all")
} }
graphCmd.AddArguments("-C", "-M", "--date=iso"). graphCmd.AddArguments("-C", "-M", "--date=iso-strict").
AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page). AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page).
AddOptionFormat("--pretty=format:%s", format) AddOptionFormat("--pretty=format:%s", format)

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
asymkey_model "code.gitea.io/gitea/models/asymkey" asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -192,6 +193,14 @@ var RelationCommit = &Commit{
Row: -1, Row: -1,
} }
func parseGitTime(timeStr string) time.Time {
t, err := time.Parse(time.RFC3339, timeStr)
if err != nil {
return time.Unix(0, 0)
}
return t
}
// NewCommit creates a new commit from a provided line // NewCommit creates a new commit from a provided line
func NewCommit(row, column int, line []byte) (*Commit, error) { func NewCommit(row, column int, line []byte) (*Commit, error) {
data := bytes.SplitN(line, []byte("|"), 5) data := bytes.SplitN(line, []byte("|"), 5)
@ -206,7 +215,7 @@ func NewCommit(row, column int, line []byte) (*Commit, error) {
// 1 matches git log --pretty=format:%H => commit hash // 1 matches git log --pretty=format:%H => commit hash
Rev: string(data[1]), Rev: string(data[1]),
// 2 matches git log --pretty=format:%ad => author date (format respects --date= option) // 2 matches git log --pretty=format:%ad => author date (format respects --date= option)
Date: string(data[2]), Date: parseGitTime(string(data[2])),
// 3 matches git log --pretty=format:%h => abbreviated commit hash // 3 matches git log --pretty=format:%h => abbreviated commit hash
ShortRev: string(data[3]), ShortRev: string(data[3]),
// 4 matches git log --pretty=format:%s => subject // 4 matches git log --pretty=format:%s => subject
@ -245,7 +254,7 @@ type Commit struct {
Column int Column int
Refs []git.Reference Refs []git.Reference
Rev string Rev string
Date string Date time.Time
ShortRev string ShortRev string
Subject string Subject string
} }

View File

@ -27,7 +27,7 @@ func (du *DateUtils) AbsoluteShort(time any) template.HTML {
// AbsoluteLong renders in "January 01, 2006" format // AbsoluteLong renders in "January 01, 2006" format
func (du *DateUtils) AbsoluteLong(time any) template.HTML { func (du *DateUtils) AbsoluteLong(time any) template.HTML {
return dateTimeFormat("short", time) return dateTimeFormat("long", time)
} }
// FullTime renders in "Jan 01, 2006 20:33:44" format // FullTime renders in "Jan 01, 2006 20:33:44" format

View File

@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
@ -1046,18 +1047,11 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
return return
} }
var deadlineUnix timeutil.TimeStamp deadlineUnix, _ := common.ParseAPIDeadlineToEndOfDay(form.Deadline)
var deadline time.Time
if form.Deadline != nil && !form.Deadline.IsZero() {
deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
23, 59, 59, 0, time.Local)
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
return return
} }
ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()})
} }

View File

@ -7,7 +7,6 @@ package repo
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@ -16,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
) )
@ -155,16 +155,16 @@ func CreateMilestone(ctx *context.APIContext) {
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.CreateMilestoneOption) form := web.GetForm(ctx).(*api.CreateMilestoneOption)
if form.Deadline == nil { var deadlineUnix int64
defaultDeadline, _ := time.ParseInLocation("2006-01-02", "9999-12-31", time.Local) if form.Deadline != nil {
form.Deadline = &defaultDeadline deadlineUnix = form.Deadline.Unix()
} }
milestone := &issues_model.Milestone{ milestone := &issues_model.Milestone{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Name: form.Title, Name: form.Title,
Content: form.Description, Content: form.Description,
DeadlineUnix: timeutil.TimeStamp(form.Deadline.Unix()), DeadlineUnix: timeutil.TimeStamp(deadlineUnix),
} }
if form.State == "closed" { if form.State == "closed" {
@ -225,9 +225,7 @@ func EditMilestone(ctx *context.APIContext) {
if form.Description != nil { if form.Description != nil {
milestone.Content = *form.Description milestone.Content = *form.Description
} }
if form.Deadline != nil && !form.Deadline.IsZero() { milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
milestone.DeadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
oldIsClosed := milestone.IsClosed oldIsClosed := milestone.IsClosed
if form.State != nil { if form.State != nil {

View File

@ -0,0 +1,31 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package common
import (
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
)
func ParseDeadlineDateToEndOfDay(date string) (timeutil.TimeStamp, error) {
if date == "" {
return 0, nil
}
deadline, err := time.ParseInLocation("2006-01-02", date, setting.DefaultUILocation)
if err != nil {
return 0, err
}
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
return timeutil.TimeStamp(deadline.Unix()), nil
}
func ParseAPIDeadlineToEndOfDay(t *time.Time) (timeutil.TimeStamp, error) {
if t == nil || t.IsZero() || t.Unix() == 0 {
return 0, nil
}
deadline := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, setting.DefaultUILocation)
return timeutil.TimeStamp(deadline.Unix()), nil
}

View File

@ -17,7 +17,6 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
activities_model "code.gitea.io/gitea/models/activities" activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -45,9 +44,9 @@ import (
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/templates/vars"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/routers/utils"
shared_user "code.gitea.io/gitea/routers/web/shared/user" shared_user "code.gitea.io/gitea/routers/web/shared/user"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
@ -2329,7 +2328,6 @@ func UpdateIssueContent(ctx *context.Context) {
// UpdateIssueDeadline updates an issue deadline // UpdateIssueDeadline updates an issue deadline
func UpdateIssueDeadline(ctx *context.Context) { func UpdateIssueDeadline(ctx *context.Context) {
form := web.GetForm(ctx).(*api.EditDeadlineOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
if err != nil { if err != nil {
if issues_model.IsErrIssueNotExist(err) { if issues_model.IsErrIssueNotExist(err) {
@ -2345,20 +2343,13 @@ func UpdateIssueDeadline(ctx *context.Context) {
return return
} }
var deadlineUnix timeutil.TimeStamp deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline"))
var deadline time.Time
if form.Deadline != nil && !form.Deadline.IsZero() {
deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
23, 59, 59, 0, time.Local)
deadlineUnix = timeutil.TimeStamp(deadline.Unix())
}
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
return return
} }
ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) ctx.JSONRedirect("")
} }
// UpdateIssueMilestone change issue's milestone // UpdateIssueMilestone change issue's milestone

View File

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
@ -16,8 +15,8 @@ import (
"code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/issue" "code.gitea.io/gitea/services/issue"
@ -134,22 +133,18 @@ func NewMilestonePost(ctx *context.Context) {
return return
} }
if len(form.Deadline) == 0 { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
form.Deadline = "9999-12-31"
}
deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
if err != nil { if err != nil {
ctx.Data["Err_Deadline"] = true ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return return
} }
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location()) if err := issues_model.NewMilestone(ctx, &issues_model.Milestone{
if err = issues_model.NewMilestone(ctx, &issues_model.Milestone{
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
Name: form.Title, Name: form.Title,
Content: form.Content, Content: form.Content,
DeadlineUnix: timeutil.TimeStamp(deadline.Unix()), DeadlineUnix: deadlineUnix,
}); err != nil { }); err != nil {
ctx.ServerError("NewMilestone", err) ctx.ServerError("NewMilestone", err)
return return
@ -194,17 +189,13 @@ func EditMilestonePost(ctx *context.Context) {
return return
} }
if len(form.Deadline) == 0 { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
form.Deadline = "9999-12-31"
}
deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
if err != nil { if err != nil {
ctx.Data["Err_Deadline"] = true ctx.Data["Err_Deadline"] = true
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
return return
} }
deadline = time.Date(deadline.Year(), deadline.Month(), deadline.Day(), 23, 59, 59, 0, deadline.Location())
m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id"))
if err != nil { if err != nil {
if issues_model.IsErrMilestoneNotExist(err) { if issues_model.IsErrMilestoneNotExist(err) {
@ -216,7 +207,7 @@ func EditMilestonePost(ctx *context.Context) {
} }
m.Name = form.Title m.Name = form.Title
m.Content = form.Content m.Content = form.Content
m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix()) m.DeadlineUnix = deadlineUnix
if err = issues_model.UpdateMilestone(ctx, m, m.IsClosed); err != nil { if err = issues_model.UpdateMilestone(ctx, m, m.IsClosed); err != nil {
ctx.ServerError("UpdateMilestone", err) ctx.ServerError("UpdateMilestone", err)
return return

View File

@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) {
m.Group("/{index}", func() { m.Group("/{index}", func() {
m.Post("/title", repo.UpdateIssueTitle) m.Post("/title", repo.UpdateIssueTitle)
m.Post("/content", repo.UpdateIssueContent) m.Post("/content", repo.UpdateIssueContent)
m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Post("/deadline", repo.UpdateIssueDeadline)
m.Post("/watch", repo.IssueWatch) m.Post("/watch", repo.IssueWatch)
m.Post("/ref", repo.UpdateIssueRef) m.Post("/ref", repo.UpdateIssueRef)
m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin) m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin)

View File

@ -260,7 +260,7 @@ func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone {
if m.IsClosed { if m.IsClosed {
apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
} }
if m.DeadlineUnix.Year() < 9999 { if m.DeadlineUnix > 0 {
apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
} }
return apiMilestone return apiMilestone

View File

@ -56,7 +56,7 @@
{{end}} {{end}}
{{end}} {{end}}
</span> </span>
<span class="author tw-flex tw-items-center tw-mr-2 tw-gap-[1px]"> <span class="author tw-flex tw-items-center tw-mr-2 tw-gap-1">
{{$userName := $commit.Commit.Author.Name}} {{$userName := $commit.Commit.Author.Name}}
{{if $commit.User}} {{if $commit.User}}
{{if and $commit.User.FullName DefaultShowFullName}} {{if and $commit.User.FullName DefaultShowFullName}}

View File

@ -30,9 +30,9 @@
<div class="field {{if .Err_Deadline}}error{{end}}"> <div class="field {{if .Err_Deadline}}error{{end}}">
<label> <label>
{{ctx.Locale.Tr "repo.milestones.due_date"}} {{ctx.Locale.Tr "repo.milestones.due_date"}}
<a id="clear-date">{{ctx.Locale.Tr "repo.milestones.clear"}}</a> <a id="milestone-clear-deadline">{{ctx.Locale.Tr "repo.milestones.clear"}}</a>
</label> </label>
<input type="date" id="deadline" name="deadline" value="{{.deadline}}" placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}"> <input type="date" name="deadline" class="tw-w-auto" value="{{.deadline}}" placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}">
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label> <label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label>

View File

@ -358,44 +358,31 @@
<div class="divider"></div> <div class="divider"></div>
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span> <span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span>
<div class="ui form" id="deadline-loader"> <div class="ui form tw-mt-2">
<div class="ui negative message tw-hidden" id="deadline-err-invalid-date"> {{if .Issue.DeadlineUnix}}
{{svg "octicon-x" 16 "close icon"}} <div class="tw-flex tw-justify-between tw-items-center tw-gap-2">
{{ctx.Locale.Tr "repo.issues.due_date_invalid"}}
</div>
{{if ne .Issue.DeadlineUnix 0}}
<p>
<div class="tw-flex tw-justify-between tw-items-center">
<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}> <div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
{{svg "octicon-calendar" 16 "tw-mr-2"}} {{svg "octicon-calendar"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}}
{{DateUtils.AbsoluteLong .Issue.DeadlineUnix}}
</div> </div>
<div> <div class="flex-text-block">
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil" 16 "tw-mr-1"}}</a> <a class="issue-due-edit muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_edit"}}">{{svg "octicon-pencil"}}</a>
<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a> <a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a>
{{end}} {{end}}
</div> </div>
</div> </div>
</p>
{{else}} {{else}}
<p>{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}</p> {{ctx.Locale.Tr "repo.issues.due_date_not_set"}}
{{end}} {{end}}
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
<div {{if ne .Issue.DeadlineUnix 0}} class="tw-hidden"{{end}} id="deadlineForm"> <form class="ui fluid action input issue-due-form form-fetch-action tw-mt-2 {{if .Issue.DeadlineUnix}}tw-hidden{{end}}"
<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form"> method="post" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline"
>
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<input required placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}} type="date" name="deadlineDate" id="deadlineDate"> <input required type="date" name="deadline" placeholder="{{ctx.Locale.Tr "repo.issues.due_date_form"}}" {{if .Issue.DeadlineUnix}}value="{{.Issue.DeadlineUnix.FormatDate}}"{{end}}>
<button class="ui icon button"> <button class="ui icon button">{{Iif .Issue.DeadlineUnix (svg "octicon-pencil") (svg "octicon-plus")}}</button>
{{if ne .Issue.DeadlineUnix 0}}
{{svg "octicon-pencil"}}
{{else}}
{{svg "octicon-plus"}}
{{end}}
</button>
</form> </form>
</div>
{{end}} {{end}}
</div> </div>

View File

@ -59,6 +59,7 @@ func TestAPIIssuesMilestone(t *testing.T) {
DecodeJSON(t, resp, &apiMilestone) DecodeJSON(t, resp, &apiMilestone)
assert.Equal(t, "wow", apiMilestone.Title) assert.Equal(t, "wow", apiMilestone.Title)
assert.Equal(t, structs.StateClosed, apiMilestone.State) assert.Equal(t, structs.StateClosed, apiMilestone.State)
assert.Nil(t, apiMilestone.Deadline)
var apiMilestones []structs.Milestone var apiMilestones []structs.Milestone
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s", owner.Name, repo.Name, "all")). req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s", owner.Name, repo.Name, "all")).
@ -66,6 +67,7 @@ func TestAPIIssuesMilestone(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiMilestones) DecodeJSON(t, resp, &apiMilestones)
assert.Len(t, apiMilestones, 4) assert.Len(t, apiMilestones, 4)
assert.Nil(t, apiMilestones[0].Deadline)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%s", owner.Name, repo.Name, apiMilestones[2].Title)). req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%s", owner.Name, repo.Name, apiMilestones[2].Title)).
AddTokenAuth(token) AddTokenAuth(token)

View File

@ -657,26 +657,21 @@ func TestUpdateIssueDeadline(t *testing.T) {
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext)) assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix)) assert.Equal(t, "2002-04-20", issueBefore.DeadlineUnix.FormatDate())
assert.Equal(t, api.StateOpen, issueBefore.State()) assert.Equal(t, api.StateOpen, issueBefore.State())
session := loginUser(t, owner.Name) session := loginUser(t, owner.Name)
urlStr := fmt.Sprintf("%s/%s/issues/%d/deadline?_csrf=%s", owner.Name, repoBefore.Name, issueBefore.Index, GetUserCSRFToken(t, session))
issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"})
req := NewRequest(t, "GET", issueURL) session.MakeRequest(t, req, http.StatusOK)
resp := session.MakeRequest(t, req, http.StatusOK) issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
htmlDoc := NewHTMLParser(t, resp.Body) assert.EqualValues(t, "2022-04-06", issueAfter.DeadlineUnix.FormatDate())
urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF() req = NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": ""})
req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{ session.MakeRequest(t, req, http.StatusOK)
"due_date": "2022-04-06T00:00:00.000Z", issueAfter = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
}) assert.True(t, issueAfter.DeadlineUnix.IsZero())
resp = session.MakeRequest(t, req, http.StatusCreated)
var apiIssue api.IssueDeadline
DecodeJSON(t, resp, &apiIssue)
assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
} }
func TestIssueReferenceURL(t *testing.T) { func TestIssueReferenceURL(t *testing.T) {

View File

@ -3,6 +3,7 @@ import {POST} from '../modules/fetch.ts';
import {updateIssuesMeta} from './repo-common.ts'; import {updateIssuesMeta} from './repo-common.ts';
import {svg} from '../svg.ts'; import {svg} from '../svg.ts';
import {htmlEscape} from 'escape-goat'; import {htmlEscape} from 'escape-goat';
import {toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments // if there are draft comments, confirm before reloading, to avoid losing comments
function reloadConfirmDraftComment() { function reloadConfirmDraftComment() {
@ -258,8 +259,22 @@ function selectItem(select_id, input_id) {
}); });
} }
function initRepoIssueDue() {
const form = document.querySelector<HTMLFormElement>('.issue-due-form');
if (!form) return;
const deadline = form.querySelector<HTMLInputElement>('input[name=deadline]');
document.querySelector('.issue-due-edit')?.addEventListener('click', () => {
toggleElem(form);
});
document.querySelector('.issue-due-remove')?.addEventListener('click', () => {
deadline.value = '';
form.dispatchEvent(new Event('submit', {cancelable: true, bubbles: true}));
});
}
export function initRepoIssueSidebar() { export function initRepoIssueSidebar() {
initBranchSelector(); initBranchSelector();
initRepoIssueDue();
// Init labels and assignees // Init labels and assignees
initListSubmits('select-label', 'labels'); initListSubmits('select-label', 'labels');

View File

@ -43,52 +43,6 @@ export function initRepoIssueTimeTracking() {
}); });
} }
async function updateDeadline(deadlineString) {
hideElem('#deadline-err-invalid-date');
document.querySelector('#deadline-loader')?.classList.add('is-loading');
let realDeadline = null;
if (deadlineString !== '') {
const newDate = Date.parse(deadlineString);
if (Number.isNaN(newDate)) {
document.querySelector('#deadline-loader')?.classList.remove('is-loading');
showElem('#deadline-err-invalid-date');
return false;
}
realDeadline = new Date(newDate);
}
try {
const response = await POST(document.querySelector('#update-issue-deadline-form').getAttribute('action'), {
data: {due_date: realDeadline},
});
if (response.ok) {
window.location.reload();
} else {
throw new Error('Invalid response');
}
} catch (error) {
console.error(error);
document.querySelector('#deadline-loader').classList.remove('is-loading');
showElem('#deadline-err-invalid-date');
}
}
export function initRepoIssueDue() {
$(document).on('click', '.issue-due-edit', () => {
toggleElem('#deadlineForm');
});
$(document).on('click', '.issue-due-remove', () => {
updateDeadline('');
});
$(document).on('submit', '.issue-due-form', () => {
updateDeadline($('#deadlineDate').val());
return false;
});
}
/** /**
* @param {HTMLElement} item * @param {HTMLElement} item
*/ */

View File

@ -1,11 +1,9 @@
import $ from 'jquery';
export function initRepoMilestone() { export function initRepoMilestone() {
// Milestones const page = document.querySelector('.repository.new.milestone');
if ($('.repository.new.milestone').length > 0) { if (!page) return;
$('#clear-date').on('click', () => {
$('#deadline').val(''); const deadline = page.querySelector<HTMLInputElement>('form input[name=deadline]');
return false; document.querySelector('#milestone-clear-deadline').addEventListener('click', () => {
deadline.value = '';
}); });
} }
}

View File

@ -25,7 +25,6 @@ import {initPdfViewer} from './render/pdf.ts';
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
import { import {
initRepoIssueDue,
initRepoIssueReferenceRepositorySearch, initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking, initRepoIssueTimeTracking,
initRepoIssueWipTitle, initRepoIssueWipTitle,
@ -181,7 +180,6 @@ onDomReady(() => {
initRepoEditor, initRepoEditor,
initRepoGraphGit, initRepoGraphGit,
initRepoIssueContentHistory, initRepoIssueContentHistory,
initRepoIssueDue,
initRepoIssueList, initRepoIssueList,
initRepoIssueSidebarList, initRepoIssueSidebarList,
initArchivedLabelHandler, initArchivedLabelHandler,