From e524f63d58900557d7d57fc3bcd19d9facc8b8ee Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 2 Nov 2024 19:20:22 +0800 Subject: [PATCH 1/3] Fix git error handling (#32401) --- modules/git/error.go | 40 ++++++-------------------- modules/git/repo_attribute.go | 4 +-- routers/api/v1/repo/repo.go | 7 ++--- routers/private/default_branch.go | 11 +++---- routers/web/repo/githttp.go | 2 +- services/gitdiff/gitdiff.go | 2 +- services/mirror/mirror_pull.go | 10 ++----- services/repository/branch.go | 7 +---- services/repository/files/temp_repo.go | 37 +++++++++--------------- services/repository/push.go | 4 +-- 10 files changed, 35 insertions(+), 89 deletions(-) diff --git a/modules/git/error.go b/modules/git/error.go index 91d25eca69..10fb37be07 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -4,28 +4,14 @@ package git import ( + "context" + "errors" "fmt" "strings" - "time" "code.gitea.io/gitea/modules/util" ) -// ErrExecTimeout error when exec timed out -type ErrExecTimeout struct { - Duration time.Duration -} - -// IsErrExecTimeout if some error is ErrExecTimeout -func IsErrExecTimeout(err error) bool { - _, ok := err.(ErrExecTimeout) - return ok -} - -func (err ErrExecTimeout) Error() string { - return fmt.Sprintf("execution is timeout [duration: %v]", err.Duration) -} - // ErrNotExist commit not exist error type ErrNotExist struct { ID string @@ -62,21 +48,6 @@ func IsErrBadLink(err error) bool { return ok } -// ErrUnsupportedVersion error when required git version not matched -type ErrUnsupportedVersion struct { - Required string -} - -// IsErrUnsupportedVersion if some error is ErrUnsupportedVersion -func IsErrUnsupportedVersion(err error) bool { - _, ok := err.(ErrUnsupportedVersion) - return ok -} - -func (err ErrUnsupportedVersion) Error() string { - return fmt.Sprintf("Operation requires higher version [required: %s]", err.Required) -} - // ErrBranchNotExist represents a "BranchNotExist" kind of error. type ErrBranchNotExist struct { Name string @@ -185,3 +156,10 @@ func IsErrMoreThanOne(err error) bool { func (err *ErrMoreThanOne) Error() string { return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) } + +func IsErrCanceledOrKilled(err error) bool { + // When "cancel()" a git command's context, the returned error of "Run()" could be one of them: + // - context.Canceled + // - *exec.ExitError: "signal: killed" + return err != nil && (errors.Is(err, context.Canceled) || err.Error() == "signal: killed") +} diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 84f85d1b1a..90eb783fe8 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -166,9 +166,7 @@ func (c *CheckAttributeReader) Run() error { Stdout: c.stdOut, Stderr: stdErr, }) - if err != nil && // If there is an error we need to return but: - c.ctx.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded) - err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed + if err != nil && !IsErrCanceledOrKilled(err) { return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) } return nil diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 698ba3cc94..69a95dd5a5 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -21,7 +21,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" @@ -739,10 +738,8 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) { if !repo.IsEmpty { if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, *opts.DefaultBranch); err != nil { - if !git.IsErrUnsupportedVersion(err) { - ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) - return err - } + ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err) + return err } updateRepoLicense = true } diff --git a/routers/private/default_branch.go b/routers/private/default_branch.go index 03c19c8ff4..8f6e9084df 100644 --- a/routers/private/default_branch.go +++ b/routers/private/default_branch.go @@ -8,7 +8,6 @@ import ( "net/http" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/private" gitea_context "code.gitea.io/gitea/services/context" @@ -23,12 +22,10 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { ctx.Repo.Repository.DefaultBranch = branch if err := gitrepo.SetDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { - if !git.IsErrUnsupportedVersion(err) { - ctx.JSON(http.StatusInternalServerError, private.Response{ - Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), - }) - return - } + ctx.JSON(http.StatusInternalServerError, private.Response{ + Err: fmt.Sprintf("Unable to set default branch on repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return } if err := repo_model.UpdateDefaultBranch(ctx, ctx.Repo.Repository); err != nil { diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index ee1ec1fd0c..58a2bdbab1 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -467,7 +467,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) { Stderr: &stderr, UseContextTimeout: true, }); err != nil { - if err.Error() != "signal: killed" { + if !git.IsErrCanceledOrKilled(err) { log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String()) } return diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index cf7da0707b..bb1722039e 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1162,7 +1162,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi Dir: repoPath, Stdout: writer, Stderr: stderr, - }); err != nil && err.Error() != "signal: killed" { + }); err != nil && !git.IsErrCanceledOrKilled(err) { log.Error("error during GetDiff(git diff dir: %s): %v, stderr: %s", repoPath, err, stderr.String()) } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 654a50d11e..a81fe6e9bd 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -613,14 +613,8 @@ func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, re } // Update the git repository default branch if err := gitrepo.SetDefaultBranch(ctx, m.Repo, m.Repo.DefaultBranch); err != nil { - if !git.IsErrUnsupportedVersion(err) { - log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) - desc := fmt.Sprintf("Failed to update default branch of underlying git repository '%s': %v", m.Repo.RepoPath(), err) - if err = system_model.CreateRepositoryNotice(desc); err != nil { - log.Error("CreateRepositoryNotice: %v", err) - } - return false - } + log.Error("Failed to update default branch of underlying git repository %-v. Error: %v", m.Repo, err) + return false } m.Repo.IsEmpty = false // Update the is empty and default_branch columns diff --git a/services/repository/branch.go b/services/repository/branch.go index 67df4363e4..600ba96e92 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -602,12 +602,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR log.Error("CancelPreviousJobs: %v", err) } - if err := gitrepo.SetDefaultBranch(ctx, repo, newBranchName); err != nil { - if !git.IsErrUnsupportedVersion(err) { - return err - } - } - return nil + return gitrepo.SetDefaultBranch(ctx, repo, newBranchName) }); err != nil { return err } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index ce98967e79..30ab22db1e 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -339,8 +339,7 @@ func (t *TemporaryUploadRepository) Push(doer *user_model.User, commitHash, bran func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { stdoutReader, stdoutWriter, err := os.Pipe() if err != nil { - log.Error("Unable to open stdout pipe: %v", err) - return nil, fmt.Errorf("Unable to open stdout pipe: %w", err) + return nil, fmt.Errorf("unable to open stdout pipe: %w", err) } defer func() { _ = stdoutReader.Close() @@ -348,9 +347,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { }() stderr := new(bytes.Buffer) var diff *gitdiff.Diff - var finalErr error - - if err := git.NewCommand(t.ctx, "diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD"). + err = git.NewCommand(t.ctx, "diff-index", "--src-prefix=\\a/", "--dst-prefix=\\b/", "--cached", "-p", "HEAD"). Run(&git.RunOpts{ Timeout: 30 * time.Second, Dir: t.basePath, @@ -359,27 +356,19 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() defer cancel() - diff, finalErr = gitdiff.ParsePatch(t.ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "") - if finalErr != nil { - log.Error("ParsePatch: %v", finalErr) - cancel() - } + var diffErr error + diff, diffErr = gitdiff.ParsePatch(t.ctx, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader, "") _ = stdoutReader.Close() - return finalErr + if diffErr != nil { + // if the diffErr is not nil, it will be returned as the error of "Run()" + return fmt.Errorf("ParsePatch: %w", diffErr) + } + return nil }, - }); err != nil { - if finalErr != nil { - log.Error("Unable to ParsePatch in temporary repo %s (%s). Error: %v", t.repo.FullName(), t.basePath, finalErr) - return nil, finalErr - } - - // If the process exited early, don't error - if err != context.Canceled { - log.Error("Unable to run diff-index pipeline in temporary repo %s (%s). Error: %v\nStderr: %s", - t.repo.FullName(), t.basePath, err, stderr) - return nil, fmt.Errorf("Unable to run diff-index pipeline in temporary repo %s. Error: %w\nStderr: %s", - t.repo.FullName(), err, stderr) - } + }) + if err != nil && !git.IsErrCanceledOrKilled(err) { + log.Error("Unable to diff-index in temporary repo %s (%s). Error: %v\nStderr: %s", t.repo.FullName(), t.basePath, err, stderr) + return nil, fmt.Errorf("unable to run diff-index pipeline in temporary repo: %w", err) } diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(t.ctx, t.basePath, git.TrustedCmdArgs{"--cached"}, "HEAD") diff --git a/services/repository/push.go b/services/repository/push.go index 8b81588c07..36c7927ab6 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -183,9 +183,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { repo.IsEmpty = false if repo.DefaultBranch != setting.Repository.DefaultBranch { if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { - if !git.IsErrUnsupportedVersion(err) { - return err - } + return err } } // Update the is empty and default_branch columns From 259811617ba15c77ddd89360178a59251d611af2 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 3 Nov 2024 05:04:53 +0800 Subject: [PATCH 2/3] Replace DateTime with proper functions (#32402) Follow #32383 This PR cleans up the "Deadline" usages in templates, make them call `ParseLegacy` first to get a `Time` struct then display by `DateUtils`. Now it should be pretty clear how "deadline string" works, it makes it possible to do further refactoring and correcting. --- modules/templates/helper.go | 4 +-- modules/templates/util_date.go | 26 ++++++++++++++ .../util_date_test.go} | 36 ++++++++++--------- modules/timeutil/datetime.go | 12 ++----- templates/repo/issue/milestone_issues.tmpl | 2 +- templates/repo/issue/milestones.tmpl | 2 +- .../repo/issue/view_content/comments.tmpl | 10 +++--- templates/user/dashboard/milestones.tmpl | 2 +- 8 files changed, 59 insertions(+), 35 deletions(-) rename modules/{timeutil/datetime_test.go => templates/util_date_test.go} (61%) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 5038e8a132..a5168541d8 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -54,7 +54,7 @@ func NewFuncMap() template.FuncMap { "StringUtils": NewStringUtils, "SliceUtils": NewSliceUtils, "JsonUtils": NewJsonUtils, - "DateUtils": NewDateUtils, // TODO: to be replaced by DateUtils + "DateUtils": NewDateUtils, // ----------------------------------------------------------------- // svg / avatar / icon / color @@ -71,7 +71,7 @@ func NewFuncMap() template.FuncMap { "CountFmt": base.FormatNumberSI, "TimeSince": timeutil.TimeSince, "TimeSinceUnix": timeutil.TimeSinceUnix, - "DateTime": timeutil.DateTime, + "DateTime": dateTimeLegacy, // for backward compatibility only, do not use it anymore "Sec2Time": util.SecToTime, "LoadTimes": func(startTime time.Time) string { return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index ec48a7e4be..45dd8da02f 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -6,7 +6,9 @@ package templates import ( "context" "html/template" + "time" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" ) @@ -32,3 +34,27 @@ func (du *DateUtils) AbsoluteLong(time any) template.HTML { func (du *DateUtils) FullTime(time any) template.HTML { return timeutil.DateTime("full", time) } + +// ParseLegacy parses the datetime in legacy format, eg: "2016-01-02" in server's timezone. +// It shouldn't be used in new code. New code should use Time or TimeStamp as much as possible. +func (du *DateUtils) ParseLegacy(datetime string) time.Time { + return parseLegacy(datetime) +} + +func parseLegacy(datetime string) time.Time { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + t, _ = time.ParseInLocation(time.DateOnly, datetime, setting.DefaultUILocation) + } + return t +} + +func dateTimeLegacy(format string, datetime any, _ ...string) template.HTML { + if !setting.IsProd || setting.IsInTesting { + panic("dateTimeLegacy is for backward compatibility only, do not use it in new code") + } + if s, ok := datetime.(string); ok { + datetime = parseLegacy(s) + } + return timeutil.DateTime(format, datetime) +} diff --git a/modules/timeutil/datetime_test.go b/modules/templates/util_date_test.go similarity index 61% rename from modules/timeutil/datetime_test.go rename to modules/templates/util_date_test.go index ac2ce35ba2..96c3776d39 100644 --- a/modules/timeutil/datetime_test.go +++ b/modules/templates/util_date_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package timeutil +package templates import ( "testing" @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -16,32 +17,35 @@ import ( func TestDateTime(t *testing.T) { testTz, _ := time.LoadLocation("America/New_York") defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + defer test.MockVariableValue(&setting.IsInTesting, false)() + + du := NewDateUtils(nil) refTimeStr := "2018-01-01T00:00:00Z" refDateStr := "2018-01-01" refTime, _ := time.Parse(time.RFC3339, refTimeStr) - refTimeStamp := TimeStamp(refTime.Unix()) + refTimeStamp := timeutil.TimeStamp(refTime.Unix()) - assert.EqualValues(t, "-", DateTime("short", nil)) - assert.EqualValues(t, "-", DateTime("short", 0)) - assert.EqualValues(t, "-", DateTime("short", time.Time{})) - assert.EqualValues(t, "-", DateTime("short", TimeStamp(0))) + assert.EqualValues(t, "-", du.AbsoluteShort(nil)) + assert.EqualValues(t, "-", du.AbsoluteShort(0)) + assert.EqualValues(t, "-", du.AbsoluteShort(time.Time{})) + assert.EqualValues(t, "-", du.AbsoluteShort(timeutil.TimeStamp(0))) - actual := DateTime("short", "invalid") - assert.EqualValues(t, `invalid`, actual) + actual := dateTimeLegacy("short", "invalid") + assert.EqualValues(t, `-`, actual) - actual = DateTime("short", refTimeStr) - assert.EqualValues(t, `2018-01-01T00:00:00Z`, actual) - - actual = DateTime("short", refTime) + actual = dateTimeLegacy("short", refTimeStr) assert.EqualValues(t, `2018-01-01`, actual) - actual = DateTime("short", refDateStr) - assert.EqualValues(t, `2018-01-01`, actual) + actual = du.AbsoluteShort(refTime) + assert.EqualValues(t, `2018-01-01`, actual) - actual = DateTime("short", refTimeStamp) + actual = dateTimeLegacy("short", refDateStr) + assert.EqualValues(t, `2018-01-01`, actual) + + actual = du.AbsoluteShort(refTimeStamp) assert.EqualValues(t, `2017-12-31`, actual) - actual = DateTime("full", refTimeStamp) + actual = du.FullTime(refTimeStamp) assert.EqualValues(t, `2017-12-31 19:00:00 -05:00`, actual) } diff --git a/modules/timeutil/datetime.go b/modules/timeutil/datetime.go index c089173560..664e0320b0 100644 --- a/modules/timeutil/datetime.go +++ b/modules/timeutil/datetime.go @@ -12,9 +12,7 @@ import ( ) // DateTime renders an absolute time HTML element by datetime. -func DateTime(format string, datetime any, extraAttrs ...string) template.HTML { - // TODO: remove the extraAttrs argument, it's not used in any call to DateTime - +func DateTime(format string, datetime any) template.HTML { if p, ok := datetime.(*time.Time); ok { datetime = *p } @@ -34,9 +32,6 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML { switch v := datetime.(type) { case nil: return "-" - case string: - datetimeEscaped = html.EscapeString(v) - textEscaped = datetimeEscaped case time.Time: if v.IsZero() || v.Unix() == 0 { return "-" @@ -51,10 +46,7 @@ func DateTime(format string, datetime any, extraAttrs ...string) template.HTML { panic(fmt.Sprintf("Unsupported time type %T", datetime)) } - attrs := make([]string, 0, 10+len(extraAttrs)) - attrs = append(attrs, extraAttrs...) - attrs = append(attrs, `weekday=""`, `year="numeric"`) - + attrs := []string{`weekday=""`, `year="numeric"`} switch format { case "short", "long": // date only attrs = append(attrs, `month="`+format+`"`, `day="numeric"`) diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index 5bae6fc6d5..01bd944f46 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -38,7 +38,7 @@ {{if .Milestone.DeadlineString}} {{svg "octicon-calendar"}} - {{DateTime "short" .Milestone.DeadlineString}} + {{ctx.DateUtils.AbsoluteShort (.Milestone.DeadlineString|ctx.DateUtils.ParseLegacy)}} {{else}} {{svg "octicon-calendar"}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index bce7ad8717..8b84659d10 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -59,7 +59,7 @@ {{if .DeadlineString}} {{svg "octicon-calendar" 14}} - {{DateTime "short" .DeadlineString}} + {{ctx.DateUtils.AbsoluteShort (.DeadlineString|ctx.DateUtils.ParseLegacy)}} {{else}} {{svg "octicon-calendar" 14}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 57abbeb8f7..9324959bed 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -296,7 +296,8 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_added" (DateTime "long" .Content) $createdStr}} + {{$dueDate := ctx.DateUtils.AbsoluteLong (.Content|ctx.DateUtils.ParseLegacy)}} + {{ctx.Locale.Tr "repo.issues.due_date_added" $dueDate $createdStr}} {{else if eq .Type 17}} @@ -307,8 +308,8 @@ {{template "shared/user/authorlink" .Poster}} {{$parsedDeadline := StringUtils.Split .Content "|"}} {{if eq (len $parsedDeadline) 2}} - {{$from := DateTime "long" (index $parsedDeadline 1)}} - {{$to := DateTime "long" (index $parsedDeadline 0)}} + {{$to := ctx.DateUtils.AbsoluteLong ((index $parsedDeadline 0)|ctx.DateUtils.ParseLegacy)}} + {{$from := ctx.DateUtils.AbsoluteLong ((index $parsedDeadline 1)|ctx.DateUtils.ParseLegacy)}} {{ctx.Locale.Tr "repo.issues.due_date_modified" $to $from $createdStr}} {{end}} @@ -319,7 +320,8 @@ {{template "shared/user/avatarlink" dict "user" .Poster}} {{template "shared/user/authorlink" .Poster}} - {{ctx.Locale.Tr "repo.issues.due_date_remove" (DateTime "long" .Content) $createdStr}} + {{$dueDate := ctx.DateUtils.AbsoluteLong (.Content|ctx.DateUtils.ParseLegacy)}} + {{ctx.Locale.Tr "repo.issues.due_date_remove" $dueDate $createdStr}} {{else if eq .Type 19}} diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 71ff8dba3f..e01eca7c74 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -116,7 +116,7 @@ {{if .DeadlineString}} {{svg "octicon-calendar" 14}} - {{DateTime "short" .DeadlineString}} + {{ctx.DateUtils.AbsoluteShort (.DeadlineString|ctx.DateUtils.ParseLegacy)}} {{else}} {{svg "octicon-calendar" 14}} From f2a6df03d953d608a5cac19cb9fa2c6d62dbe0e3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 3 Nov 2024 19:00:12 +0800 Subject: [PATCH 3/3] Refactor repo legacy (#32404) Only move code, no unnecessary logic change. (There are many problems in old code, but changing them is not in this PR's scope) Co-authored-by: Giteabot --- web_src/js/features/repo-common.ts | 11 + web_src/js/features/repo-issue-list.ts | 2 +- web_src/js/features/repo-issue-sidebar.ts | 274 +++++++++++++++++ web_src/js/features/repo-issue.ts | 32 +- web_src/js/features/repo-legacy.ts | 342 +--------------------- web_src/js/features/repo-milestone.ts | 11 + web_src/js/features/repo-new.ts | 14 + web_src/js/features/repo-settings.ts | 48 ++- web_src/js/index.ts | 11 +- 9 files changed, 383 insertions(+), 362 deletions(-) create mode 100644 web_src/js/features/repo-issue-sidebar.ts create mode 100644 web_src/js/features/repo-milestone.ts create mode 100644 web_src/js/features/repo-new.ts diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index de967ffba0..c7d84de9f0 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -90,3 +90,14 @@ export function initRepoCommonFilterSearchDropdown(selector) { message: {noResults: $dropdown[0].getAttribute('data-no-results')}, }); } + +export async function updateIssuesMeta(url, action, issue_ids, id) { + try { + const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); + if (!response.ok) { + throw new Error('Failed to update issues meta'); + } + } catch (error) { + console.error(error); + } +} diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 134304617b..caf517c5e0 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import {updateIssuesMeta} from './repo-issue.ts'; +import {updateIssuesMeta} from './repo-common.ts'; import {toggleElem, hideElem, isElemHidden} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {confirmModal} from './comp/ConfirmModal.ts'; diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts new file mode 100644 index 0000000000..f33e192f29 --- /dev/null +++ b/web_src/js/features/repo-issue-sidebar.ts @@ -0,0 +1,274 @@ +import $ from 'jquery'; +import {POST} from '../modules/fetch.ts'; +import {updateIssuesMeta} from './repo-common.ts'; +import {svg} from '../svg.ts'; +import {htmlEscape} from 'escape-goat'; + +// if there are draft comments, confirm before reloading, to avoid losing comments +function reloadConfirmDraftComment() { + const commentTextareas = [ + document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'), + document.querySelector('#comment-form textarea'), + ]; + for (const textarea of commentTextareas) { + // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds. + // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy. + if (textarea && textarea.value.trim().length > 10) { + textarea.parentElement.scrollIntoView(); + if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) { + return; + } + break; + } + } + window.location.reload(); +} + +function initBranchSelector() { + const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); + if (!elSelectBranch) return; + + const urlUpdateIssueRef = elSelectBranch.getAttribute('data-url-update-issueref'); + const $selectBranch = $(elSelectBranch); + const $branchMenu = $selectBranch.find('.reference-list-menu'); + $branchMenu.find('.item:not(.no-select)').on('click', async function (e) { + e.preventDefault(); + const selectedValue = this.getAttribute('data-id'); // eg: "refs/heads/my-branch" + const selectedText = this.getAttribute('data-name'); // eg: "my-branch" + if (urlUpdateIssueRef) { + // for existing issue, send request to update issue ref, and reload page + try { + await POST(urlUpdateIssueRef, {data: new URLSearchParams({ref: selectedValue})}); + window.location.reload(); + } catch (error) { + console.error(error); + } + } else { + // for new issue, only update UI&form, do not send request/reload + const selectedHiddenSelector = this.getAttribute('data-id-selector'); + document.querySelector(selectedHiddenSelector).value = selectedValue; + elSelectBranch.querySelector('.text-branch-name').textContent = selectedText; + } + }); +} + +// List submits +function initListSubmits(selector, outerSelector) { + const $list = $(`.ui.${outerSelector}.list`); + const $noSelect = $list.find('.no-select'); + const $listMenu = $(`.${selector} .menu`); + let hasUpdateAction = $listMenu.data('action') === 'update'; + const items = {}; + + $(`.${selector}`).dropdown({ + 'action': 'nothing', // do not hide the menu if user presses Enter + fullTextSearch: 'exact', + async onHide() { + hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var + if (hasUpdateAction) { + // TODO: Add batch functionality and make this 1 network request. + const itemEntries = Object.entries(items); + for (const [elementId, item] of itemEntries) { + await updateIssuesMeta( + item['update-url'], + item.action, + item['issue-id'], + elementId, + ); + } + if (itemEntries.length) { + reloadConfirmDraftComment(); + } + } + }, + }); + + $listMenu.find('.item:not(.no-select)').on('click', function (e) { + e.preventDefault(); + if (this.classList.contains('ban-change')) { + return false; + } + + hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var + + const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment + const scope = this.getAttribute('data-scope'); + + $(this).parent().find('.item').each(function () { + if (scope) { + // Enable only clicked item for scoped labels + if (this.getAttribute('data-scope') !== scope) { + return true; + } + if (this !== clickedItem && !this.classList.contains('checked')) { + return true; + } + } else if (this !== clickedItem) { + // Toggle for other labels + return true; + } + + if (this.classList.contains('checked')) { + $(this).removeClass('checked'); + $(this).find('.octicon-check').addClass('tw-invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'detach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } + } + } else { + $(this).addClass('checked'); + $(this).find('.octicon-check').removeClass('tw-invisible'); + if (hasUpdateAction) { + if (!($(this).data('id') in items)) { + items[$(this).data('id')] = { + 'update-url': $listMenu.data('update-url'), + action: 'attach', + 'issue-id': $listMenu.data('issue-id'), + }; + } else { + delete items[$(this).data('id')]; + } + } + } + }); + + // TODO: Which thing should be done for choosing review requests + // to make chosen items be shown on time here? + if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { + return false; + } + + const listIds = []; + $(this).parent().find('.item').each(function () { + if (this.classList.contains('checked')) { + listIds.push($(this).data('id')); + $($(this).data('id-selector')).removeClass('tw-hidden'); + } else { + $($(this).data('id-selector')).addClass('tw-hidden'); + } + }); + if (!listIds.length) { + $noSelect.removeClass('tw-hidden'); + } else { + $noSelect.addClass('tw-hidden'); + } + $($(this).parent().data('id')).val(listIds.join(',')); + return false; + }); + $listMenu.find('.no-select.item').on('click', function (e) { + e.preventDefault(); + if (hasUpdateAction) { + (async () => { + await updateIssuesMeta( + $listMenu.data('update-url'), + 'clear', + $listMenu.data('issue-id'), + '', + ); + reloadConfirmDraftComment(); + })(); + } + + $(this).parent().find('.item').each(function () { + $(this).removeClass('checked'); + $(this).find('.octicon-check').addClass('tw-invisible'); + }); + + if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { + return false; + } + + $list.find('.item').each(function () { + $(this).addClass('tw-hidden'); + }); + $noSelect.removeClass('tw-hidden'); + $($(this).parent().data('id')).val(''); + }); +} + +function selectItem(select_id, input_id) { + const $menu = $(`${select_id} .menu`); + const $list = $(`.ui${select_id}.list`); + const hasUpdateAction = $menu.data('action') === 'update'; + + $menu.find('.item:not(.no-select)').on('click', function () { + $(this).parent().find('.item').each(function () { + $(this).removeClass('selected active'); + }); + + $(this).addClass('selected active'); + if (hasUpdateAction) { + (async () => { + await updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ); + reloadConfirmDraftComment(); + })(); + } + + let icon = ''; + if (input_id === '#milestone_id') { + icon = svg('octicon-milestone', 18, 'tw-mr-2'); + } else if (input_id === '#project_id') { + icon = svg('octicon-project', 18, 'tw-mr-2'); + } else if (input_id === '#assignee_id') { + icon = `avatar`; + } + + $list.find('.selected').html(` + + ${icon} + ${htmlEscape(this.textContent)} + + `); + + $(`.ui${select_id}.list .no-select`).addClass('tw-hidden'); + $(input_id).val($(this).data('id')); + }); + $menu.find('.no-select.item').on('click', function () { + $(this).parent().find('.item:not(.no-select)').each(function () { + $(this).removeClass('selected active'); + }); + + if (hasUpdateAction) { + (async () => { + await updateIssuesMeta( + $menu.data('update-url'), + '', + $menu.data('issue-id'), + $(this).data('id'), + ); + reloadConfirmDraftComment(); + })(); + } + + $list.find('.selected').html(''); + $list.find('.no-select').removeClass('tw-hidden'); + $(input_id).val(''); + }); +} + +export function initRepoIssueSidebar() { + initBranchSelector(); + + // Init labels and assignees + initListSubmits('select-label', 'labels'); + initListSubmits('select-assignees', 'assignees'); + initListSubmits('select-assignees-modify', 'assignees'); + initListSubmits('select-reviewers-modify', 'assignees'); + + // Milestone, Assignee, Project + selectItem('.select-project', '#project_id'); + selectItem('.select-milestone', '#milestone_id'); + selectItem('.select-assignee', '#assignee_id'); +} diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index 520f2081b0..a9a65cdc81 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -7,6 +7,8 @@ import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} fr import {toAbsoluteUrl} from '../utils.ts'; import {GET, POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; +import {initRepoIssueSidebar} from './repo-issue-sidebar.ts'; +import {updateIssuesMeta} from './repo-common.ts'; const {appSubUrl} = window.config; @@ -369,17 +371,6 @@ export function initRepoIssueWipTitle() { }); } -export async function updateIssuesMeta(url, action, issue_ids, id) { - try { - const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); - if (!response.ok) { - throw new Error('Failed to update issues meta'); - } - } catch (error) { - console.error(error); - } -} - export function initRepoIssueComments() { if (!$('.repository.view.issue .timeline').length) return; @@ -665,7 +656,7 @@ export function initRepoIssueBranchSelect() { }); } -export async function initSingleCommentEditor($commentForm) { +async function initSingleCommentEditor($commentForm) { // pages: // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content) // * issue/pr view page: with comment form, has status-button and comment-button @@ -687,7 +678,7 @@ export async function initSingleCommentEditor($commentForm) { syncUiState(); } -export function initIssueTemplateCommentEditors($commentForm) { +function initIssueTemplateCommentEditors($commentForm) { // pages: // * new issue with issue template const $comboFields = $commentForm.find('.combo-editor-dropzone'); @@ -733,3 +724,18 @@ export function initArchivedLabelHandler() { toggleElem(label, label.classList.contains('checked')); } } + +export function initRepoCommentFormAndSidebar() { + const $commentForm = $('.comment.form'); + if (!$commentForm.length) return; + + if ($commentForm.find('.field.combo-editor-dropzone').length) { + // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form + initIssueTemplateCommentEditors($commentForm); + } else if ($commentForm.find('.combo-markdown-editor').length) { + // it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message + initSingleCommentEditor($commentForm); + } + + initRepoIssueSidebar(); +} diff --git a/web_src/js/features/repo-legacy.ts b/web_src/js/features/repo-legacy.ts index 5844037770..6be3252a2b 100644 --- a/web_src/js/features/repo-legacy.ts +++ b/web_src/js/features/repo-legacy.ts @@ -1,13 +1,12 @@ import $ from 'jquery'; import { + initRepoCommentFormAndSidebar, initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete, initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue, initRepoIssueTitleEdit, initRepoIssueWipToggle, - initRepoPullRequestUpdate, updateIssuesMeta, initIssueTemplateCommentEditors, initSingleCommentEditor, + initRepoPullRequestUpdate, } from './repo-issue.ts'; import {initUnicodeEscapeButton} from './repo-unicode-escape.ts'; -import {svg} from '../svg.ts'; -import {htmlEscape} from 'escape-goat'; import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue'; import { initRepoCloneLink, initRepoCommonBranchOrTagDropdown, initRepoCommonFilterSearchDropdown, @@ -16,32 +15,13 @@ import {initCitationFileCopyContent} from './citation.ts'; import {initCompLabelEdit} from './comp/LabelEdit.ts'; import {initRepoDiffConversationNav} from './repo-diff.ts'; import {initCompReactionSelector} from './comp/ReactionSelector.ts'; -import {initRepoSettingBranches} from './repo-settings.ts'; +import {initRepoSettings} from './repo-settings.ts'; import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.ts'; import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.ts'; import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts'; -import {POST} from '../modules/fetch.ts'; import {initRepoIssueCommentEdit} from './repo-issue-edit.ts'; - -// if there are draft comments, confirm before reloading, to avoid losing comments -function reloadConfirmDraftComment() { - const commentTextareas = [ - document.querySelector('.edit-content-zone:not(.tw-hidden) textarea'), - document.querySelector('#comment-form textarea'), - ]; - for (const textarea of commentTextareas) { - // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds. - // But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy. - if (textarea && textarea.value.trim().length > 10) { - textarea.parentElement.scrollIntoView(); - if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) { - return; - } - break; - } - } - window.location.reload(); -} +import {initRepoMilestone} from './repo-milestone.ts'; +import {initRepoNew} from './repo-new.ts'; export function initBranchSelectorTabs() { const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); @@ -56,320 +36,16 @@ export function initBranchSelectorTabs() { }); } -export function initRepoCommentForm() { - const $commentForm = $('.comment.form'); - if (!$commentForm.length) return; - - if ($commentForm.find('.field.combo-editor-dropzone').length) { - // at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form - initIssueTemplateCommentEditors($commentForm); - } else if ($commentForm.find('.combo-markdown-editor').length) { - // it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message - initSingleCommentEditor($commentForm); - } - - function initBranchSelector() { - const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); - if (!elSelectBranch) return; - - const urlUpdateIssueRef = elSelectBranch.getAttribute('data-url-update-issueref'); - const $selectBranch = $(elSelectBranch); - const $branchMenu = $selectBranch.find('.reference-list-menu'); - $branchMenu.find('.item:not(.no-select)').on('click', async function (e) { - e.preventDefault(); - const selectedValue = this.getAttribute('data-id'); // eg: "refs/heads/my-branch" - const selectedText = this.getAttribute('data-name'); // eg: "my-branch" - if (urlUpdateIssueRef) { - // for existing issue, send request to update issue ref, and reload page - try { - await POST(urlUpdateIssueRef, {data: new URLSearchParams({ref: selectedValue})}); - window.location.reload(); - } catch (error) { - console.error(error); - } - } else { - // for new issue, only update UI&form, do not send request/reload - const selectedHiddenSelector = this.getAttribute('data-id-selector'); - document.querySelector(selectedHiddenSelector).value = selectedValue; - elSelectBranch.querySelector('.text-branch-name').textContent = selectedText; - } - }); - } - - initBranchSelector(); - - // List submits - function initListSubmits(selector, outerSelector) { - const $list = $(`.ui.${outerSelector}.list`); - const $noSelect = $list.find('.no-select'); - const $listMenu = $(`.${selector} .menu`); - let hasUpdateAction = $listMenu.data('action') === 'update'; - const items = {}; - - $(`.${selector}`).dropdown({ - 'action': 'nothing', // do not hide the menu if user presses Enter - fullTextSearch: 'exact', - async onHide() { - hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var - if (hasUpdateAction) { - // TODO: Add batch functionality and make this 1 network request. - const itemEntries = Object.entries(items); - for (const [elementId, item] of itemEntries) { - await updateIssuesMeta( - item['update-url'], - item.action, - item['issue-id'], - elementId, - ); - } - if (itemEntries.length) { - reloadConfirmDraftComment(); - } - } - }, - }); - - $listMenu.find('.item:not(.no-select)').on('click', function (e) { - e.preventDefault(); - if (this.classList.contains('ban-change')) { - return false; - } - - hasUpdateAction = $listMenu.data('action') === 'update'; // Update the var - - const clickedItem = this; // eslint-disable-line unicorn/no-this-assignment - const scope = this.getAttribute('data-scope'); - - $(this).parent().find('.item').each(function () { - if (scope) { - // Enable only clicked item for scoped labels - if (this.getAttribute('data-scope') !== scope) { - return true; - } - if (this !== clickedItem && !this.classList.contains('checked')) { - return true; - } - } else if (this !== clickedItem) { - // Toggle for other labels - return true; - } - - if (this.classList.contains('checked')) { - $(this).removeClass('checked'); - $(this).find('.octicon-check').addClass('tw-invisible'); - if (hasUpdateAction) { - if (!($(this).data('id') in items)) { - items[$(this).data('id')] = { - 'update-url': $listMenu.data('update-url'), - action: 'detach', - 'issue-id': $listMenu.data('issue-id'), - }; - } else { - delete items[$(this).data('id')]; - } - } - } else { - $(this).addClass('checked'); - $(this).find('.octicon-check').removeClass('tw-invisible'); - if (hasUpdateAction) { - if (!($(this).data('id') in items)) { - items[$(this).data('id')] = { - 'update-url': $listMenu.data('update-url'), - action: 'attach', - 'issue-id': $listMenu.data('issue-id'), - }; - } else { - delete items[$(this).data('id')]; - } - } - } - }); - - // TODO: Which thing should be done for choosing review requests - // to make chosen items be shown on time here? - if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { - return false; - } - - const listIds = []; - $(this).parent().find('.item').each(function () { - if (this.classList.contains('checked')) { - listIds.push($(this).data('id')); - $($(this).data('id-selector')).removeClass('tw-hidden'); - } else { - $($(this).data('id-selector')).addClass('tw-hidden'); - } - }); - if (!listIds.length) { - $noSelect.removeClass('tw-hidden'); - } else { - $noSelect.addClass('tw-hidden'); - } - $($(this).parent().data('id')).val(listIds.join(',')); - return false; - }); - $listMenu.find('.no-select.item').on('click', function (e) { - e.preventDefault(); - if (hasUpdateAction) { - (async () => { - await updateIssuesMeta( - $listMenu.data('update-url'), - 'clear', - $listMenu.data('issue-id'), - '', - ); - reloadConfirmDraftComment(); - })(); - } - - $(this).parent().find('.item').each(function () { - $(this).removeClass('checked'); - $(this).find('.octicon-check').addClass('tw-invisible'); - }); - - if (selector === 'select-reviewers-modify' || selector === 'select-assignees-modify') { - return false; - } - - $list.find('.item').each(function () { - $(this).addClass('tw-hidden'); - }); - $noSelect.removeClass('tw-hidden'); - $($(this).parent().data('id')).val(''); - }); - } - - // Init labels and assignees - initListSubmits('select-label', 'labels'); - initListSubmits('select-assignees', 'assignees'); - initListSubmits('select-assignees-modify', 'assignees'); - initListSubmits('select-reviewers-modify', 'assignees'); - - function selectItem(select_id, input_id) { - const $menu = $(`${select_id} .menu`); - const $list = $(`.ui${select_id}.list`); - const hasUpdateAction = $menu.data('action') === 'update'; - - $menu.find('.item:not(.no-select)').on('click', function () { - $(this).parent().find('.item').each(function () { - $(this).removeClass('selected active'); - }); - - $(this).addClass('selected active'); - if (hasUpdateAction) { - (async () => { - await updateIssuesMeta( - $menu.data('update-url'), - '', - $menu.data('issue-id'), - $(this).data('id'), - ); - reloadConfirmDraftComment(); - })(); - } - - let icon = ''; - if (input_id === '#milestone_id') { - icon = svg('octicon-milestone', 18, 'tw-mr-2'); - } else if (input_id === '#project_id') { - icon = svg('octicon-project', 18, 'tw-mr-2'); - } else if (input_id === '#assignee_id') { - icon = `avatar`; - } - - $list.find('.selected').html(` - - ${icon} - ${htmlEscape(this.textContent)} - - `); - - $(`.ui${select_id}.list .no-select`).addClass('tw-hidden'); - $(input_id).val($(this).data('id')); - }); - $menu.find('.no-select.item').on('click', function () { - $(this).parent().find('.item:not(.no-select)').each(function () { - $(this).removeClass('selected active'); - }); - - if (hasUpdateAction) { - (async () => { - await updateIssuesMeta( - $menu.data('update-url'), - '', - $menu.data('issue-id'), - $(this).data('id'), - ); - reloadConfirmDraftComment(); - })(); - } - - $list.find('.selected').html(''); - $list.find('.no-select').removeClass('tw-hidden'); - $(input_id).val(''); - }); - } - - // Milestone, Assignee, Project - selectItem('.select-project', '#project_id'); - selectItem('.select-milestone', '#milestone_id'); - selectItem('.select-assignee', '#assignee_id'); -} - export function initRepository() { if (!$('.page-content.repository').length) return; initRepoBranchTagSelector('.js-branch-tag-selector'); - - // Options - if ($('.repository.settings.options').length > 0) { - // Enable or select internal/external wiki system and issue tracker. - $('.enable-system').on('change', function () { - if (this.checked) { - $($(this).data('target')).removeClass('disabled'); - if (!$(this).data('context')) $($(this).data('context')).addClass('disabled'); - } else { - $($(this).data('target')).addClass('disabled'); - if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled'); - } - }); - $('.enable-system-radio').on('change', function () { - if (this.value === 'false') { - $($(this).data('target')).addClass('disabled'); - if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled'); - } else if (this.value === 'true') { - $($(this).data('target')).removeClass('disabled'); - if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled'); - } - }); - const $trackerIssueStyleRadios = $('.js-tracker-issue-style'); - $trackerIssueStyleRadios.on('change input', () => { - const checkedVal = $trackerIssueStyleRadios.filter(':checked').val(); - $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp'); - }); - } + initRepoCommentFormAndSidebar(); // Labels initCompLabelEdit('.repository.labels'); - - // Milestones - if ($('.repository.new.milestone').length > 0) { - $('#clear-date').on('click', () => { - $('#deadline').val(''); - return false; - }); - } - - // Repo Creation - if ($('.repository.new.repo').length > 0) { - $('input[name="gitignores"], input[name="license"]').on('change', () => { - const gitignores = $('input[name="gitignores"]').val(); - const license = $('input[name="license"]').val(); - if (gitignores || license) { - document.querySelector('input[name="auto_init"]').checked = true; - } - }); - } + initRepoMilestone(); + initRepoNew(); // Compare or pull request const $repoDiff = $('.repository.diff'); @@ -380,7 +56,7 @@ export function initRepository() { initRepoCloneLink(); initCitationFileCopyContent(); - initRepoSettingBranches(); + initRepoSettings(); // Issues if ($('.repository.view.issue').length > 0) { diff --git a/web_src/js/features/repo-milestone.ts b/web_src/js/features/repo-milestone.ts new file mode 100644 index 0000000000..ddef723b48 --- /dev/null +++ b/web_src/js/features/repo-milestone.ts @@ -0,0 +1,11 @@ +import $ from 'jquery'; + +export function initRepoMilestone() { + // Milestones + if ($('.repository.new.milestone').length > 0) { + $('#clear-date').on('click', () => { + $('#deadline').val(''); + return false; + }); + } +} diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts new file mode 100644 index 0000000000..22d8c8a47b --- /dev/null +++ b/web_src/js/features/repo-new.ts @@ -0,0 +1,14 @@ +import $ from 'jquery'; + +export function initRepoNew() { + // Repo Creation + if ($('.repository.new.repo').length > 0) { + $('input[name="gitignores"], input[name="license"]').on('change', () => { + const gitignores = $('input[name="gitignores"]').val(); + const license = $('input[name="license"]').val(); + if (gitignores || license) { + document.querySelector('input[name="auto_init"]').checked = true; + } + }); + } +} diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 97211d035e..34a3b635b2 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -6,7 +6,7 @@ import {POST} from '../modules/fetch.ts'; const {appSubUrl, csrfToken} = window.config; -export function initRepoSettingsCollaboration() { +function initRepoSettingsCollaboration() { // Change collaborator access mode for (const dropdownEl of queryElems('.page-content.repository .ui.dropdown.access-mode')) { const textEl = dropdownEl.querySelector(':scope > .text'); @@ -43,7 +43,7 @@ export function initRepoSettingsCollaboration() { } } -export function initRepoSettingSearchTeamBox() { +function initRepoSettingsSearchTeamBox() { const searchTeamBox = document.querySelector('#search-team-box'); if (!searchTeamBox) return; @@ -69,13 +69,13 @@ export function initRepoSettingSearchTeamBox() { }); } -export function initRepoSettingGitHook() { +function initRepoSettingsGitHook() { if (!$('.edit.githook').length) return; const filename = document.querySelector('.hook-filename').textContent; - const _promise = createMonaco($('#content')[0], filename, {language: 'shell'}); + createMonaco($('#content')[0], filename, {language: 'shell'}); } -export function initRepoSettingBranches() { +function initRepoSettingsBranches() { if (!document.querySelector('.repository.settings.branches')) return; for (const el of document.querySelectorAll('.toggle-target-enabled')) { @@ -117,3 +117,41 @@ export function initRepoSettingBranches() { markMatchedStatusChecks(); document.querySelector('#status_check_contexts').addEventListener('input', onInputDebounce(markMatchedStatusChecks)); } + +function initRepoSettingsOptions() { + if ($('.repository.settings.options').length > 0) { + // Enable or select internal/external wiki system and issue tracker. + $('.enable-system').on('change', function () { + if (this.checked) { + $($(this).data('target')).removeClass('disabled'); + if (!$(this).data('context')) $($(this).data('context')).addClass('disabled'); + } else { + $($(this).data('target')).addClass('disabled'); + if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled'); + } + }); + $('.enable-system-radio').on('change', function () { + if (this.value === 'false') { + $($(this).data('target')).addClass('disabled'); + if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled'); + } else if (this.value === 'true') { + $($(this).data('target')).removeClass('disabled'); + if ($(this).data('context') !== undefined) $($(this).data('context')).addClass('disabled'); + } + }); + const $trackerIssueStyleRadios = $('.js-tracker-issue-style'); + $trackerIssueStyleRadios.on('change input', () => { + const checkedVal = $trackerIssueStyleRadios.filter(':checked').val(); + $('#tracker-issue-style-regex-box').toggleClass('disabled', checkedVal !== 'regexp'); + }); + } +} + +export function initRepoSettings() { + if (!document.querySelector('.page-content.repository.settings')) return; + initRepoSettingsOptions(); + initRepoSettingsBranches(); + initRepoSettingsCollaboration(); + initRepoSettingsSearchTeamBox(); + initRepoSettingsGitHook(); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index f63d199488..08d8997fd1 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -43,11 +43,6 @@ import {initSshKeyFormParser} from './features/sshkey-helper.ts'; import {initUserSettings} from './features/user-settings.ts'; import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; -import { - initRepoSettingGitHook, - initRepoSettingsCollaboration, - initRepoSettingSearchTeamBox, -} from './features/repo-settings.ts'; import {initRepoDiffView} from './features/repo-diff.ts'; import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.ts'; import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; @@ -59,7 +54,7 @@ import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; import {initRepoBranchButton} from './features/repo-branch.ts'; import {initCommonOrganization} from './features/common-organization.ts'; import {initRepoWikiForm} from './features/repo-wiki.ts'; -import {initRepoCommentForm, initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; +import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; import {initCopyContent} from './features/copycontent.ts'; import {initCaptcha} from './features/captcha.ts'; import {initRepositoryActionView} from './components/RepoActionView.vue'; @@ -180,7 +175,6 @@ onDomReady(() => { initRepoArchiveLinks, initRepoBranchButton, initRepoCodeView, - initRepoCommentForm, initBranchSelectorTabs, initRepoEllipsisButton, initRepoDiffCommitBranchesAndTags, @@ -202,9 +196,6 @@ onDomReady(() => { initRepoPullRequestReview, initRepoRelease, initRepoReleaseNew, - initRepoSettingGitHook, - initRepoSettingSearchTeamBox, - initRepoSettingsCollaboration, initRepoTemplateSearch, initRepoTopicBar, initRepoWikiForm,