diff --git a/models/actions/schedule_spec_test.go b/models/actions/schedule_spec_test.go index 0c26fce4b2c5e..57221461dfec5 100644 --- a/models/actions/schedule_spec_test.go +++ b/models/actions/schedule_spec_test.go @@ -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) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index e86fb3ccb1c52..aee6ef937a625 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -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,14 +1047,7 @@ 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 diff --git a/routers/api/v1/repo/milestone.go b/routers/api/v1/repo/milestone.go index 67e3ed829efee..78907c85a5847 100644 --- a/routers/api/v1/repo/milestone.go +++ b/routers/api/v1/repo/milestone.go @@ -15,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" ) @@ -224,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 { diff --git a/routers/common/deadline.go b/routers/common/deadline.go new file mode 100644 index 0000000000000..152e94597bc6a --- /dev/null +++ b/routers/common/deadline.go @@ -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 +} diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 507b5af9d904a..21eb71e9f3393 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -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 diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index a6d0df919c4d7..af394a822d411 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -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" @@ -18,6 +17,7 @@ import ( "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,24 +134,18 @@ func NewMilestonePost(ctx *context.Context) { return } - var deadlineUnix int64 - if len(form.Deadline) > 0 { - deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) - 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()) - deadlineUnix = deadline.Unix() + 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 } if err := issues_model.NewMilestone(ctx, &issues_model.Milestone{ RepoID: ctx.Repo.Repository.ID, Name: form.Title, Content: form.Content, - DeadlineUnix: timeutil.TimeStamp(deadlineUnix), + DeadlineUnix: deadlineUnix, }); err != nil { ctx.ServerError("NewMilestone", err) return @@ -196,17 +190,11 @@ func EditMilestonePost(ctx *context.Context) { return } - var deadlineUnix int64 - if len(form.Deadline) > 0 { - deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local) - 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()) - deadlineUnix = deadline.Unix() + 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 } m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":id")) diff --git a/routers/web/web.go b/routers/web/web.go index 83d116babd769..ebedea73f1907 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/templates/repo/issue/milestone_new.tmpl b/templates/repo/issue/milestone_new.tmpl index 9f32df00e39f0..736a75d73a1b3 100644 --- a/templates/repo/issue/milestone_new.tmpl +++ b/templates/repo/issue/milestone_new.tmpl @@ -30,9 +30,9 @@
- +
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index c168e98785eda..a89a2bb17e439 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -358,44 +358,31 @@
{{ctx.Locale.Tr "repo.issues.due_date"}} -
-
- {{svg "octicon-x" 16 "close icon"}} - {{ctx.Locale.Tr "repo.issues.due_date_invalid"}} -
- {{if ne .Issue.DeadlineUnix 0}} -

-

-
- {{svg "octicon-calendar" 16 "tw-mr-2"}} - {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}} -
-
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} - {{svg "octicon-pencil" 16 "tw-mr-1"}} - {{svg "octicon-trash"}} - {{end}} -
+
+ {{if .Issue.DeadlineUnix}} +
+
+ {{svg "octicon-calendar"}} {{DateUtils.AbsoluteLong .Issue.DeadlineUnix}} +
+
+ {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} + {{svg "octicon-pencil"}} + {{svg "octicon-trash"}} + {{end}}
-

+
{{else}} -

{{ctx.Locale.Tr "repo.issues.due_date_not_set"}}

+ {{ctx.Locale.Tr "repo.issues.due_date_not_set"}} {{end}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} -
-
- {{$.CsrfTokenHtml}} - - -
-
+
+ {{$.CsrfTokenHtml}} + + +
{{end}}
diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go index 38f36d27a74c1..2d00752302ff2 100644 --- a/tests/integration/api_issue_milestone_test.go +++ b/tests/integration/api_issue_milestone_test.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "testing" - "time" auth_model "code.gitea.io/gitea/models/auth" issues_model "code.gitea.io/gitea/models/issues" @@ -60,7 +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.Equal(t, (*time.Time)(nil), apiMilestone.Deadline) + 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")). @@ -68,7 +67,7 @@ func TestAPIIssuesMilestone(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiMilestones) assert.Len(t, apiMilestones, 4) - assert.Equal(t, (*time.Time)(nil), apiMilestones[0].Deadline) + 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) diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts index f33e192f291a4..478dafa81974a 100644 --- a/web_src/js/features/repo-issue-sidebar.ts +++ b/web_src/js/features/repo-issue-sidebar.ts @@ -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 {initRepoIssueDue} from './repo-issue.ts'; // if there are draft comments, confirm before reloading, to avoid losing comments function reloadConfirmDraftComment() { @@ -260,6 +261,7 @@ function selectItem(select_id, input_id) { export function initRepoIssueSidebar() { initBranchSelector(); + initRepoIssueDue(); // Init labels and assignees initListSubmits('select-label', 'labels'); diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index a9a65cdc819bc..552feb0617158 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -43,49 +43,16 @@ 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(''); + const form = document.querySelector('.issue-due-form'); + if (!form) return; + const deadline = form.querySelector('input[name=deadline]'); + document.querySelector('.issue-due-edit')?.addEventListener('click', () => { + toggleElem(form); }); - $(document).on('submit', '.issue-due-form', () => { - updateDeadline($('#deadlineDate').val()); - return false; + document.querySelector('.issue-due-remove')?.addEventListener('click', () => { + deadline.value = ''; + form.dispatchEvent(new Event('submit', {cancelable: true, bubbles: true})); }); } diff --git a/web_src/js/features/repo-milestone.ts b/web_src/js/features/repo-milestone.ts index ddef723b48c85..ee704ea2e06a4 100644 --- a/web_src/js/features/repo-milestone.ts +++ b/web_src/js/features/repo-milestone.ts @@ -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('form input[name=deadline]'); + document.querySelector('#milestone-clear-deadline').addEventListener('click', () => { + deadline.value = ''; + }); } diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 08d8997fd1816..487aac97aa3a5 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -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,