mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	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:
		| @@ -7,19 +7,17 @@ import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestActionScheduleSpec_Parse(t *testing.T) { | ||||
| 	// Mock the local timezone is not UTC | ||||
| 	local := time.Local | ||||
| 	tz, err := time.LoadLocation("Asia/Shanghai") | ||||
| 	require.NoError(t, err) | ||||
| 	defer func() { | ||||
| 		time.Local = local | ||||
| 	}() | ||||
| 	time.Local = tz | ||||
| 	defer test.MockVariableValue(&time.Local, tz)() | ||||
|  | ||||
| 	now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") | ||||
| 	require.NoError(t, err) | ||||
|   | ||||
| @@ -84,10 +84,9 @@ func (m *Milestone) BeforeUpdate() { | ||||
| // this object. | ||||
| func (m *Milestone) AfterLoad() { | ||||
| 	m.NumOpenIssues = m.NumIssues - m.NumClosedIssues | ||||
| 	if m.DeadlineUnix.Year() == 9999 { | ||||
| 	if m.DeadlineUnix == 0 { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	m.DeadlineString = m.DeadlineUnix.FormatDate() | ||||
| 	if m.IsClosed { | ||||
| 		m.IsOverdue = m.ClosedDateUnix >= m.DeadlineUnix | ||||
|   | ||||
| @@ -364,6 +364,7 @@ func prepareMigrationTasks() []*migration { | ||||
| 		newMigration(304, "Add index for release sha1", v1_23.AddIndexForReleaseSha1), | ||||
| 		newMigration(305, "Add Repository Licenses", v1_23.AddRepositoryLicenses), | ||||
| 		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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								models/migrations/v1_23/v307.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								models/migrations/v1_23/v307.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -32,7 +32,7 @@ func GetCommitGraph(r *git.Repository, page, maxAllowedColors int, hidePRRefs bo | ||||
| 		graphCmd.AddArguments("--all") | ||||
| 	} | ||||
|  | ||||
| 	graphCmd.AddArguments("-C", "-M", "--date=iso"). | ||||
| 	graphCmd.AddArguments("-C", "-M", "--date=iso-strict"). | ||||
| 		AddOptionFormat("-n %d", setting.UI.GraphMaxCommitNum*page). | ||||
| 		AddOptionFormat("--pretty=format:%s", format) | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| @@ -192,6 +193,14 @@ var RelationCommit = &Commit{ | ||||
| 	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 | ||||
| func NewCommit(row, column int, line []byte) (*Commit, error) { | ||||
| 	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 | ||||
| 		Rev: string(data[1]), | ||||
| 		// 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 | ||||
| 		ShortRev: string(data[3]), | ||||
| 		// 4 matches git log --pretty=format:%s => subject | ||||
| @@ -245,7 +254,7 @@ type Commit struct { | ||||
| 	Column       int | ||||
| 	Refs         []git.Reference | ||||
| 	Rev          string | ||||
| 	Date         string | ||||
| 	Date         time.Time | ||||
| 	ShortRev     string | ||||
| 	Subject      string | ||||
| } | ||||
|   | ||||
| @@ -27,7 +27,7 @@ func (du *DateUtils) AbsoluteShort(time any) template.HTML { | ||||
|  | ||||
| // AbsoluteLong renders in "January 01, 2006" format | ||||
| 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 | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"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/convert" | ||||
| 	issue_service "code.gitea.io/gitea/services/issue" | ||||
| @@ -1046,18 +1047,11 @@ func UpdateIssueDeadline(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var deadlineUnix timeutil.TimeStamp | ||||
| 	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()) | ||||
| 	} | ||||
|  | ||||
| 	deadlineUnix, _ := common.ParseAPIDeadlineToEndOfDay(form.Deadline) | ||||
| 	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) | ||||
| 	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: deadlineUnix.AsTimePtr()}) | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ package repo | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| @@ -16,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"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/convert" | ||||
| ) | ||||
| @@ -155,16 +155,16 @@ func CreateMilestone(ctx *context.APIContext) { | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
| 	form := web.GetForm(ctx).(*api.CreateMilestoneOption) | ||||
|  | ||||
| 	if form.Deadline == nil { | ||||
| 		defaultDeadline, _ := time.ParseInLocation("2006-01-02", "9999-12-31", time.Local) | ||||
| 		form.Deadline = &defaultDeadline | ||||
| 	var deadlineUnix int64 | ||||
| 	if form.Deadline != nil { | ||||
| 		deadlineUnix = form.Deadline.Unix() | ||||
| 	} | ||||
|  | ||||
| 	milestone := &issues_model.Milestone{ | ||||
| 		RepoID:       ctx.Repo.Repository.ID, | ||||
| 		Name:         form.Title, | ||||
| 		Content:      form.Description, | ||||
| 		DeadlineUnix: timeutil.TimeStamp(form.Deadline.Unix()), | ||||
| 		DeadlineUnix: timeutil.TimeStamp(deadlineUnix), | ||||
| 	} | ||||
|  | ||||
| 	if form.State == "closed" { | ||||
| @@ -225,9 +225,7 @@ func EditMilestone(ctx *context.APIContext) { | ||||
| 	if form.Description != nil { | ||||
| 		milestone.Content = *form.Description | ||||
| 	} | ||||
| 	if form.Deadline != nil && !form.Deadline.IsZero() { | ||||
| 		milestone.DeadlineUnix = timeutil.TimeStamp(form.Deadline.Unix()) | ||||
| 	} | ||||
| 	milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline) | ||||
|  | ||||
| 	oldIsClosed := milestone.IsClosed | ||||
| 	if form.State != nil { | ||||
|   | ||||
							
								
								
									
										31
									
								
								routers/common/deadline.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								routers/common/deadline.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -17,7 +17,6 @@ import ( | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| @@ -45,9 +44,9 @@ import ( | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/templates" | ||||
| 	"code.gitea.io/gitea/modules/templates/vars" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/common" | ||||
| 	"code.gitea.io/gitea/routers/utils" | ||||
| 	shared_user "code.gitea.io/gitea/routers/web/shared/user" | ||||
| 	asymkey_service "code.gitea.io/gitea/services/asymkey" | ||||
| @@ -2329,7 +2328,6 @@ func UpdateIssueContent(ctx *context.Context) { | ||||
|  | ||||
| // UpdateIssueDeadline updates an issue deadline | ||||
| func UpdateIssueDeadline(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*api.EditDeadlineOption) | ||||
| 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) | ||||
| 	if err != nil { | ||||
| 		if issues_model.IsErrIssueNotExist(err) { | ||||
| @@ -2345,20 +2343,13 @@ func UpdateIssueDeadline(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var deadlineUnix timeutil.TimeStamp | ||||
| 	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()) | ||||
| 	} | ||||
|  | ||||
| 	deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline")) | ||||
| 	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) | ||||
| 	ctx.JSONRedirect("") | ||||
| } | ||||
|  | ||||
| // UpdateIssueMilestone change issue's milestone | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	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/optional" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/common" | ||||
| 	"code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/issue" | ||||
| @@ -134,22 +133,18 @@ func NewMilestonePost(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(form.Deadline) == 0 { | ||||
| 		form.Deadline = "9999-12-31" | ||||
| 	} | ||||
| 	deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) | ||||
| 	deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) | ||||
| 	if err != nil { | ||||
| 		ctx.Data["Err_Deadline"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) | ||||
| 		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, | ||||
| 		Name:         form.Title, | ||||
| 		Content:      form.Content, | ||||
| 		DeadlineUnix: timeutil.TimeStamp(deadline.Unix()), | ||||
| 		DeadlineUnix: deadlineUnix, | ||||
| 	}); err != nil { | ||||
| 		ctx.ServerError("NewMilestone", err) | ||||
| 		return | ||||
| @@ -194,17 +189,13 @@ func EditMilestonePost(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(form.Deadline) == 0 { | ||||
| 		form.Deadline = "9999-12-31" | ||||
| 	} | ||||
| 	deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) | ||||
| 	deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) | ||||
| 	if err != nil { | ||||
| 		ctx.Data["Err_Deadline"] = true | ||||
| 		ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) | ||||
| 		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")) | ||||
| 	if err != nil { | ||||
| 		if issues_model.IsErrMilestoneNotExist(err) { | ||||
| @@ -216,7 +207,7 @@ func EditMilestonePost(ctx *context.Context) { | ||||
| 	} | ||||
| 	m.Name = form.Title | ||||
| 	m.Content = form.Content | ||||
| 	m.DeadlineUnix = timeutil.TimeStamp(deadline.Unix()) | ||||
| 	m.DeadlineUnix = deadlineUnix | ||||
| 	if err = issues_model.UpdateMilestone(ctx, m, m.IsClosed); err != nil { | ||||
| 		ctx.ServerError("UpdateMilestone", err) | ||||
| 		return | ||||
|   | ||||
| @@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) { | ||||
| 			m.Group("/{index}", func() { | ||||
| 				m.Post("/title", repo.UpdateIssueTitle) | ||||
| 				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("/ref", repo.UpdateIssueRef) | ||||
| 				m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin) | ||||
|   | ||||
| @@ -260,7 +260,7 @@ func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone { | ||||
| 	if m.IsClosed { | ||||
| 		apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() | ||||
| 	} | ||||
| 	if m.DeadlineUnix.Year() < 9999 { | ||||
| 	if m.DeadlineUnix > 0 { | ||||
| 		apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() | ||||
| 	} | ||||
| 	return apiMilestone | ||||
|   | ||||
| @@ -56,7 +56,7 @@ | ||||
| 							{{end}} | ||||
| 						{{end}} | ||||
| 					</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}} | ||||
| 						{{if $commit.User}} | ||||
| 							{{if and $commit.User.FullName DefaultShowFullName}} | ||||
|   | ||||
| @@ -30,9 +30,9 @@ | ||||
| 				<div class="field {{if .Err_Deadline}}error{{end}}"> | ||||
| 					<label> | ||||
| 						{{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> | ||||
| 					<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 class="field"> | ||||
| 					<label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label> | ||||
|   | ||||
| @@ -358,44 +358,31 @@ | ||||
|  | ||||
| 	<div class="divider"></div> | ||||
| 	<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.due_date"}}</strong></span> | ||||
| 	<div class="ui form" id="deadline-loader"> | ||||
| 		<div class="ui negative message tw-hidden" id="deadline-err-invalid-date"> | ||||
| 			{{svg "octicon-x" 16 "close icon"}} | ||||
| 			{{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}}> | ||||
| 						{{svg "octicon-calendar" 16 "tw-mr-2"}} | ||||
| 						{{DateUtils.AbsoluteLong .Issue.DeadlineUnix}} | ||||
| 					</div> | ||||
| 					<div> | ||||
| 						{{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-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a> | ||||
| 						{{end}} | ||||
| 					</div> | ||||
| 	<div class="ui form tw-mt-2"> | ||||
| 		{{if .Issue.DeadlineUnix}} | ||||
| 			<div class="tw-flex tw-justify-between tw-items-center tw-gap-2"> | ||||
| 				<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"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}} | ||||
| 				</div> | ||||
| 			</p> | ||||
| 				<div class="flex-text-block"> | ||||
| 					{{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"}}</a> | ||||
| 						<a class="issue-due-remove muted" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.due_date_form_remove"}}">{{svg "octicon-trash"}}</a> | ||||
| 					{{end}} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{{else}} | ||||
| 			<p>{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}</p> | ||||
| 			{{ctx.Locale.Tr "repo.issues.due_date_not_set"}} | ||||
| 		{{end}} | ||||
|  | ||||
| 		{{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" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form"> | ||||
| 					{{$.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"> | ||||
| 					<button class="ui icon button"> | ||||
| 						{{if ne .Issue.DeadlineUnix 0}} | ||||
| 							{{svg "octicon-pencil"}} | ||||
| 						{{else}} | ||||
| 							{{svg "octicon-plus"}} | ||||
| 						{{end}} | ||||
| 					</button> | ||||
| 				</form> | ||||
| 			</div> | ||||
| 			<form class="ui fluid action input issue-due-form form-fetch-action tw-mt-2 {{if .Issue.DeadlineUnix}}tw-hidden{{end}}" | ||||
| 						method="post" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" | ||||
| 			> | ||||
| 				{{$.CsrfTokenHtml}} | ||||
| 				<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">{{Iif .Issue.DeadlineUnix (svg "octicon-pencil") (svg "octicon-plus")}}</button> | ||||
| 			</form> | ||||
| 		{{end}} | ||||
| 	</div> | ||||
|  | ||||
|   | ||||
| @@ -59,6 +59,7 @@ func TestAPIIssuesMilestone(t *testing.T) { | ||||
| 	DecodeJSON(t, resp, &apiMilestone) | ||||
| 	assert.Equal(t, "wow", apiMilestone.Title) | ||||
| 	assert.Equal(t, structs.StateClosed, apiMilestone.State) | ||||
| 	assert.Nil(t, apiMilestone.Deadline) | ||||
|  | ||||
| 	var apiMilestones []structs.Milestone | ||||
| 	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) | ||||
| 	DecodeJSON(t, resp, &apiMilestones) | ||||
| 	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)). | ||||
| 		AddTokenAuth(token) | ||||
|   | ||||
| @@ -657,26 +657,21 @@ func TestUpdateIssueDeadline(t *testing.T) { | ||||
| 	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}) | ||||
| 	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()) | ||||
|  | ||||
| 	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 := NewRequest(t, "GET", issueURL) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	htmlDoc := NewHTMLParser(t, resp.Body) | ||||
| 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": "2022-04-06"}) | ||||
| 	session.MakeRequest(t, req, http.StatusOK) | ||||
| 	issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) | ||||
| 	assert.EqualValues(t, "2022-04-06", issueAfter.DeadlineUnix.FormatDate()) | ||||
|  | ||||
| 	urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF() | ||||
| 	req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{ | ||||
| 		"due_date": "2022-04-06T00:00:00.000Z", | ||||
| 	}) | ||||
|  | ||||
| 	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")) | ||||
| 	req = NewRequestWithValues(t, "POST", urlStr, map[string]string{"deadline": ""}) | ||||
| 	session.MakeRequest(t, req, http.StatusOK) | ||||
| 	issueAfter = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) | ||||
| 	assert.True(t, issueAfter.DeadlineUnix.IsZero()) | ||||
| } | ||||
|  | ||||
| func TestIssueReferenceURL(t *testing.T) { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import {POST} from '../modules/fetch.ts'; | ||||
| import {updateIssuesMeta} from './repo-common.ts'; | ||||
| import {svg} from '../svg.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {toggleElem} from '../utils/dom.ts'; | ||||
|  | ||||
| // if there are draft comments, confirm before reloading, to avoid losing comments | ||||
| 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() { | ||||
|   initBranchSelector(); | ||||
|   initRepoIssueDue(); | ||||
|  | ||||
|   // Init labels and assignees | ||||
|   initListSubmits('select-label', 'labels'); | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import $ from 'jquery'; | ||||
|  | ||||
| export function initRepoMilestone() { | ||||
|   // Milestones | ||||
|   if ($('.repository.new.milestone').length > 0) { | ||||
|     $('#clear-date').on('click', () => { | ||||
|       $('#deadline').val(''); | ||||
|       return false; | ||||
|     }); | ||||
|   } | ||||
|   const page = document.querySelector('.repository.new.milestone'); | ||||
|   if (!page) return; | ||||
|  | ||||
|   const deadline = page.querySelector<HTMLInputElement>('form input[name=deadline]'); | ||||
|   document.querySelector('#milestone-clear-deadline').addEventListener('click', () => { | ||||
|     deadline.value = ''; | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,6 @@ import {initPdfViewer} from './render/pdf.ts'; | ||||
|  | ||||
| import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; | ||||
| import { | ||||
|   initRepoIssueDue, | ||||
|   initRepoIssueReferenceRepositorySearch, | ||||
|   initRepoIssueTimeTracking, | ||||
|   initRepoIssueWipTitle, | ||||
| @@ -181,7 +180,6 @@ onDomReady(() => { | ||||
|     initRepoEditor, | ||||
|     initRepoGraphGit, | ||||
|     initRepoIssueContentHistory, | ||||
|     initRepoIssueDue, | ||||
|     initRepoIssueList, | ||||
|     initRepoIssueSidebarList, | ||||
|     initArchivedLabelHandler, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user