mirror of https://github.com/go-gitea/gitea.git
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:
parent
1887c75c35
commit
24b83ff63e
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("--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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
"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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue