···47479. Avoid unnecessary `!important` in CSS, add comments to explain why it's necessary if it can't be avoided.
484810. Avoid mixing different events in one event listener, prefer to use individual event listeners for every event.
494911. Custom event names are recommended to use `ce-` prefix.
5050-12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-df`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
5050+12. Prefer using Tailwind CSS which is available via `tw-` prefix, e.g. `tw-relative`. Gitea's helper CSS classes use `gt-` prefix (`gt-mono`), while Gitea's own private framework-level CSS classes use `g-` prefix (`g-modal-confirm`).
515113. Avoid inline scripts & styles as much as possible, it's recommended to put JS code into JS files and use CSS classes. If inline scripts & styles are unavoidable, explain the reason why it can't be avoided.
52525353### Accessibility / ARIA
···214214215215### Building and adding SVGs
216216217217-SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
217217+SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
218218219219### Building the Logo
220220
···24242525const (
2626 // DefaultAvatarClass is the default class of a rendered avatar
2727- DefaultAvatarClass = "ui avatar gt-vm"
2727+ DefaultAvatarClass = "ui avatar tw-align-middle"
2828 // DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
2929 DefaultAvatarPixelSize = 28
3030)
···4141 }
4242 logIndex += preStep.LogLength
43434444+ // lastHasRunStep is the last step that has run.
4545+ // For example,
4646+ // 1. preStep(Success) -> step1(Success) -> step2(Running) -> step3(Waiting) -> postStep(Waiting): lastHasRunStep is step1.
4747+ // 2. preStep(Success) -> step1(Success) -> step2(Success) -> step3(Success) -> postStep(Success): lastHasRunStep is step3.
4848+ // 3. preStep(Success) -> step1(Success) -> step2(Failure) -> step3 -> postStep(Waiting): lastHasRunStep is step2.
4949+ // So its Stopped is the Started of postStep when there are no more steps to run.
4450 var lastHasRunStep *actions_model.ActionTaskStep
4551 for _, step := range task.Steps {
4652 if step.Status.HasRun() {
···5662 Name: postStepName,
5763 Status: actions_model.StatusWaiting,
5864 }
5959- if task.Status.IsDone() {
6565+ // If the lastHasRunStep is the last step, or it has failed, postStep has started.
6666+ if lastHasRunStep.Status.IsFailure() || lastHasRunStep == task.Steps[len(task.Steps)-1] {
6067 postStep.LogIndex = logIndex
6168 postStep.LogLength = task.LogLength - postStep.LogIndex
6262- postStep.Status = task.Status
6369 postStep.Started = lastHasRunStep.Stopped
7070+ postStep.Status = actions_model.StatusRunning
7171+ }
7272+ if task.Status.IsDone() {
7373+ postStep.Status = task.Status
6474 postStep.Stopped = task.Stopped
6575 }
6676 ret := make([]*actions_model.ActionTaskStep, 0, len(task.Steps)+2)
···283283// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
284284func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
285285 cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
286286- AddDynamicArguments(baseBranch + "..." + targetBranch)
286286+ AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
287287 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
288288 if err != nil {
289289 return do, err
+4
modules/git/repo_stats.go
···124124 }
125125 }
126126 }
127127+ if err = scanner.Err(); err != nil {
128128+ _ = stdoutReader.Close()
129129+ return fmt.Errorf("GetCodeActivityStats scan: %w", err)
130130+ }
127131 a := make([]*CodeActivityAuthor, 0, len(authors))
128132 for _, v := range authors {
129133 a = append(a, v)
+2-2
modules/graceful/manager_unix.go
···5959 go func() {
6060 defer func() {
6161 close(startupDone)
6262- // Close the unused listeners and ignore the error here there's not much we can do with it, they're logged in the CloseProvidedListeners function
6363- _ = CloseProvidedListeners()
6262+ // Close the unused listeners
6363+ closeProvidedListeners()
6464 }()
6565 // Wait for all servers to be created
6666 g.createServerCond.L.Lock()
+2-10
modules/graceful/net_unix.go
···129129 return savedErr
130130}
131131132132-// CloseProvidedListeners closes all unused provided listeners.
133133-func CloseProvidedListeners() error {
132132+// closeProvidedListeners closes all unused provided listeners.
133133+func closeProvidedListeners() {
134134 mutex.Lock()
135135 defer mutex.Unlock()
136136- var returnableError error
137136 for _, l := range providedListeners {
138137 err := l.Close()
139138 if err != nil {
140139 log.Error("Error in closing unused provided listener: %v", err)
141141- if returnableError != nil {
142142- returnableError = fmt.Errorf("%v & %w", returnableError, err)
143143- } else {
144144- returnableError = err
145145- }
146140 }
147141 }
148142 providedListeners = []net.Listener{}
149149-150150- return returnableError
151143}
152144153145// DefaultGetListener obtains a listener for the stream-oriented local network address:
···2020}
21212222// MatchPhraseQuery generates a match phrase query for the given phrase, field and analyzer
2323-func MatchPhraseQuery(matchPhrase, field, analyzer string) *query.MatchPhraseQuery {
2323+func MatchPhraseQuery(matchPhrase, field, analyzer string, fuzziness int) *query.MatchPhraseQuery {
2424 q := bleve.NewMatchPhraseQuery(matchPhrase)
2525 q.FieldVal = field
2626 q.Analyzer = analyzer
2727- return q
2828-}
2929-3030-// PrefixQuery generates a match prefix query for the given prefix and field
3131-func PrefixQuery(matchPrefix, field string) *query.PrefixQuery {
3232- q := bleve.NewPrefixQuery(matchPrefix)
3333- q.FieldVal = field
2727+ q.Fuzziness = fuzziness
3428 return q
3529}
3630
···5454 return values
5555}
56565757-// TODO: Replace with "maps.Values" once available
5757+// TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library
5858func ValuesOfMap[K comparable, V any](m map[K]V) []V {
5959 values := make([]V, 0, len(m))
6060 for _, v := range m {
···6262 }
6363 return values
6464}
6565+6666+// TODO: Replace with "maps.Keys" once available, current it only in golang.org/x/exp/maps but not in standard library
6767+func KeysOfMap[K comparable, V any](m map[K]V) []K {
6868+ keys := make([]K, 0, len(m))
6969+ for k := range m {
7070+ keys = append(keys, k)
7171+ }
7272+ return keys
7373+}
+3
options/license/threeparttable
···11+This file may be distributed, modified, and used in other works with just
22+one restriction: modified versions must clearly indicate the modification
33+(a name change, or a displayed message, or ?).
+5-1
options/locale/locale_en-US.ini
···115115error = Error
116116error404 = The page you are trying to reach either <strong>does not exist</strong> or <strong>you are not authorized</strong> to view it.
117117go_back = Go Back
118118+invalid_data = Invalid data: %v
118119119120never = Never
120121unknown = Unknown
···13331334editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
13341335editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
13351336editor.file_already_exists = A file named "%s" already exists in this repository.
13371337+editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge.
13381338+editor.push_out_of_date = The push appears to be out of date.
13361339editor.commit_empty_file_header = Commit an empty file
13371340editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
13381341editor.no_changes_to_show = There are no changes to show.
···31333136auths.tip.dropbox = Create a new application at https://www.dropbox.com/developers/apps
31343137auths.tip.facebook = Register a new application at https://developers.facebook.com/apps and add the product "Facebook Login"
31353138auths.tip.github = Register a new OAuth application on https://github.com/settings/applications/new
31363136-auths.tip.gitlab = Register a new application on https://gitlab.com/profile/applications
31393139+auths.tip.gitlab_new = Register a new application on https://gitlab.com/-/profile/applications
31373140auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at https://console.developers.google.com/
31383141auths.tip.openid_connect = Use the OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) to specify the endpoints
31393142auths.tip.twitter = Go to https://dev.twitter.com/apps, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled
···36713674runs.workflow = Workflow
36723675runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s
36733676runs.no_matching_online_runner_helper = No matching online runner with label: %s
36773677+runs.no_job_without_needs = The workflow must contain at least one job without dependencies.
36743678runs.actor = Actor
36753679runs.status = Status
36763680runs.actors_no_select = All actors
···2727 * Maximum time expect() should wait for the condition to be met.
2828 * For example in `await expect(locator).toHaveText();`
2929 */
3030- timeout: 2000
3030+ timeout: 2000,
3131 },
32323333 /* Fail the build on CI if you accidentally left test.only in the source code. */
+3-2
routers/api/v1/repo/issue.go
···874874 }
875875 if form.State != nil {
876876 if issue.IsPull {
877877- if pr, err := issue.GetPullRequest(ctx); err != nil {
877877+ if err := issue.LoadPullRequest(ctx); err != nil {
878878 ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
879879 return
880880- } else if pr.HasMerged {
880880+ }
881881+ if issue.PullRequest.HasMerged {
881882 ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
882883 return
883884 }
···7575 updates = append(updates, option)
7676 if repo.IsEmpty && (refFullName.BranchName() == "master" || refFullName.BranchName() == "main") {
7777 // put the master/main branch first
7878+ // FIXME: It doesn't always work, since the master/main branch may not be the first batch of updates.
7979+ // If the user pushes many branches at once, the Git hook will call the internal API in batches, rather than all at once.
8080+ // See https://github.com/go-gitea/gitea/blob/cb52b17f92e2d2293f7c003649743464492bca48/cmd/hook.go#L27
8181+ // If the user executes `git push origin --all` and pushes more than 30 branches, the master/main may not be the default branch.
7882 copy(updates[1:], updates)
7983 updates[0] = option
8084 }
···104104 workflows = append(workflows, workflow)
105105 continue
106106 }
107107- // Check whether have matching runner
107107+ // The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
108108+ hasJobWithoutNeeds := false
109109+ // Check whether have matching runner and a job without "needs"
108110 for _, j := range wf.Jobs {
111111+ if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
112112+ hasJobWithoutNeeds = true
113113+ }
109114 runsOnList := j.RunsOn()
110115 for _, ro := range runsOnList {
111116 if strings.Contains(ro, "${{") {
···122127 if workflow.ErrMsg != "" {
123128 break
124129 }
130130+ }
131131+ if !hasJobWithoutNeeds {
132132+ workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
125133 }
126134 workflows = append(workflows, workflow)
127135 }
+21-5
routers/web/repo/actions/view.go
···353353 return
354354 }
355355356356- if jobIndexStr != "" {
357357- jobs = []*actions_model.ActionRunJob{job}
356356+ if jobIndexStr == "" { // rerun all jobs
357357+ for _, j := range jobs {
358358+ // if the job has needs, it should be set to "blocked" status to wait for other jobs
359359+ shouldBlock := len(j.Needs) > 0
360360+ if err := rerunJob(ctx, j, shouldBlock); err != nil {
361361+ ctx.Error(http.StatusInternalServerError, err.Error())
362362+ return
363363+ }
364364+ }
365365+ ctx.JSON(http.StatusOK, struct{}{})
366366+ return
358367 }
359368360360- for _, j := range jobs {
361361- if err := rerunJob(ctx, j); err != nil {
369369+ rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
370370+371371+ for _, j := range rerunJobs {
372372+ // jobs other than the specified one should be set to "blocked" status
373373+ shouldBlock := j.JobID != job.JobID
374374+ if err := rerunJob(ctx, j, shouldBlock); err != nil {
362375 ctx.Error(http.StatusInternalServerError, err.Error())
363376 return
364377 }
···367380 ctx.JSON(http.StatusOK, struct{}{})
368381}
369382370370-func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob) error {
383383+func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
371384 status := job.Status
372385 if !status.IsDone() {
373386 return nil
···375388376389 job.TaskID = 0
377390 job.Status = actions_model.StatusWaiting
391391+ if shouldBlock {
392392+ job.Status = actions_model.StatusBlocked
393393+ }
378394 job.Started = 0
379395 job.Stopped = 0
380396
···7979 }
8080 ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
8181 state := toCommitStatus(job.Status)
8282- if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true}); err == nil {
8282+ if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil {
8383 for _, v := range statuses {
8484 if v.Context == ctxname {
8585 if v.State == state {
+13-3
services/actions/notifier.go
···515515}
516516517517func (n *actionsNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
518518+ commitID, _ := git.NewIDFromString(opts.NewCommitID)
519519+ if commitID.IsZero() {
520520+ log.Trace("new commitID is empty")
521521+ return
522522+ }
523523+518524 ctx = withMethod(ctx, "PushCommits")
519525520526 apiPusher := convert.ToUser(ctx, pusher, nil)
···547553 apiRepo := convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeNone})
548554549555 newNotifyInput(repo, pusher, webhook_module.HookEventCreate).
550550- WithRef(refFullName.ShortName()). // FIXME: should we use a full ref name
556556+ WithRef(refFullName.String()).
551557 WithPayload(&api.CreatePayload{
552552- Ref: refFullName.ShortName(),
558558+ Ref: refFullName.String(),
553559 Sha: refID,
554560 RefType: refFullName.RefType(),
555561 Repo: apiRepo,
···566572567573 newNotifyInput(repo, pusher, webhook_module.HookEventDelete).
568574 WithPayload(&api.DeletePayload{
569569- Ref: refFullName.ShortName(),
575575+ Ref: refFullName.String(),
570576 RefType: refFullName.RefType(),
571577 PusherType: api.PusherTypeUser,
572578 Repo: apiRepo,
···623629}
624630625631func (n *actionsNotifier) DeleteRelease(ctx context.Context, doer *user_model.User, rel *repo_model.Release) {
632632+ if rel.IsTag {
633633+ // has sent same action in `PushCommits`, so skip it.
634634+ return
635635+ }
626636 ctx = withMethod(ctx, "DeleteRelease")
627637 notifyRelease(ctx, doer, rel, api.HookReleaseDeleted)
628638}
+38
services/actions/rerun.go
···11+// Copyright 2024 The Gitea Authors. All rights reserved.
22+// SPDX-License-Identifier: MIT
33+44+package actions
55+66+import (
77+ actions_model "code.gitea.io/gitea/models/actions"
88+ "code.gitea.io/gitea/modules/container"
99+)
1010+1111+// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
1212+func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob {
1313+ rerunJobs := []*actions_model.ActionRunJob{job}
1414+ rerunJobsIDSet := make(container.Set[string])
1515+ rerunJobsIDSet.Add(job.JobID)
1616+1717+ for {
1818+ found := false
1919+ for _, j := range allJobs {
2020+ if rerunJobsIDSet.Contains(j.JobID) {
2121+ continue
2222+ }
2323+ for _, need := range j.Needs {
2424+ if rerunJobsIDSet.Contains(need) {
2525+ found = true
2626+ rerunJobs = append(rerunJobs, j)
2727+ rerunJobsIDSet.Add(j.JobID)
2828+ break
2929+ }
3030+ }
3131+ }
3232+ if !found {
3333+ break
3434+ }
3535+ }
3636+3737+ return rerunJobs
3838+}
···5050 }
5151 linesInAuthorizedKeys.Add(line)
5252 }
5353- f.Close()
5353+ if err = scanner.Err(); err != nil {
5454+ return fmt.Errorf("scan: %w", err)
5555+ }
5656+ // although there is a "defer close" above, here close explicitly before the generating, because it needs to open the file for writing again
5757+ _ = f.Close()
54585559 // now we regenerate and check if there are any lines missing
5660 regenerated := &bytes.Buffer{}
···7373 {{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" "I know and must do this is dangerous operation")}}
7474 </div>
75757676- <div class="modal-buttons flex-text-block gt-fw"></div>
7676+ <div class="modal-buttons flex-text-block tw-flex-wrap"></div>
7777 <script type="module">
7878 for (const el of $('.ui.modal')) {
7979 const $btn = $('<button>').text(`${el.id}`).on('click', () => {
···6969 return pushed, deleted
7070 })
7171 })
7272+7373+ t.Run("Push to deleted branch", func(t *testing.T) {
7474+ runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) {
7575+ doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete
7676+ pushed = append(pushed, "master")
7777+7878+ doGitCreateBranch(gitPath, "branch-1")(t)
7979+ doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
8080+ pushed = append(pushed, "branch-1")
8181+8282+ // delete and restore
8383+ doGitPushTestRepository(gitPath, "origin", "--delete", "branch-1")(t)
8484+ doGitPushTestRepository(gitPath, "origin", "branch-1")(t)
8585+8686+ return pushed, deleted
8787+ })
8888+ })
7289}
73907491func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gitPath string) (pushed, deleted []string)) {
+1-1
tests/integration/issue_test.go
···807807 htmlDoc := NewHTMLParser(t, resp.Body)
808808809809 // Check that every link in the filter list has rel="nofollow".
810810- filterLinks := htmlDoc.Find(".issue-list-toolbar-right a[href*=\"/issues?q=\"]")
810810+ filterLinks := htmlDoc.Find(".issue-list-toolbar-right a[href*=\"?q=\"]")
811811 assert.True(t, filterLinks.Length() > 0)
812812 filterLinks.Each(func(i int, link *goquery.Selection) {
813813 rel, has := link.Attr("rel")
···11+/* based on Fomantic UI message module, with just the parts extracted that we use. If you find any
22+ unused rules here after refactoring, please remove them. */
33+14.ui.message {
25 background: var(--color-box-body);
36 color: var(--color-text);
···11-export async function createColorPicker($els) {
22- if (!$els || !$els.length) return;
11+import $ from 'jquery';
22+33+export async function createColorPicker(els) {
44+ if (!els.length) return;
3546 await Promise.all([
57 import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors'),
68 import(/* webpackChunkName: "minicolors" */'@claviska/jquery-minicolors/jquery.minicolors.css'),
79 ]);
81099- $els.minicolors();
1111+ return $(els).minicolors();
1012}
+7-8
web_src/js/features/common-global.js
···301301 const $this = $(this);
302302 const dataArray = $this.data();
303303 let filter = '';
304304- if ($this.attr('data-modal-id')) {
305305- filter += `#${$this.attr('data-modal-id')}`;
304304+ if (this.getAttribute('data-modal-id')) {
305305+ filter += `#${this.getAttribute('data-modal-id')}`;
306306 }
307307308308 const $dialog = $(`.delete.modal${filter}`);
···335335 const data = await response.json();
336336 window.location.href = data.redirect;
337337 }
338338- }
338338+ },
339339 }).modal('show');
340340 }
341341···352352 // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
353353 $('.show-modal').on('click', function (e) {
354354 e.preventDefault();
355355- const $el = $(this);
356356- const modalSelector = $el.attr('data-modal');
355355+ const modalSelector = this.getAttribute('data-modal');
357356 const $modal = $(modalSelector);
358357 if (!$modal.length) {
359358 throw new Error('no modal for this action');
···406405 // a '.show-panel' element can show a panel, by `data-panel="selector"`
407406 // if it has "toggle" class, it toggles the panel
408407 e.preventDefault();
409409- const sel = $(this).attr('data-panel');
408408+ const sel = this.getAttribute('data-panel');
410409 if (this.classList.contains('toggle')) {
411410 toggleElem(sel);
412411 } else {
···417416 $('.hide-panel').on('click', function (e) {
418417 // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
419418 e.preventDefault();
420420- let sel = $(this).attr('data-panel');
419419+ let sel = this.getAttribute('data-panel');
421420 if (sel) {
422421 hideElem($(sel));
423422 return;
424423 }
425425- sel = $(this).attr('data-panel-closest');
424424+ sel = this.getAttribute('data-panel-closest');
426425 if (sel) {
427426 hideElem($(this).closest(sel));
428427 return;
+15-18
web_src/js/features/common-issue-list.js
···11-import $ from 'jquery';
21import {isElemHidden, onInputDebounce, submitEventSubmitter, toggleElem} from '../utils/dom.js';
32import {GET} from '../modules/fetch.js';
43···3029}
31303231export function initCommonIssueListQuickGoto() {
3333- const $goto = $('#issue-list-quick-goto');
3434- if (!$goto.length) return;
3232+ const goto = document.getElementById('issue-list-quick-goto');
3333+ if (!goto) return;
35343636- const $form = $goto.closest('form');
3737- const $input = $form.find('input[name=q]');
3838- const repoLink = $goto.attr('data-repo-link');
3535+ const form = goto.closest('form');
3636+ const input = form.querySelector('input[name=q]');
3737+ const repoLink = goto.getAttribute('data-repo-link');
39384040- $form.on('submit', (e) => {
3939+ form.addEventListener('submit', (e) => {
4140 // if there is no goto button, or the form is submitted by non-quick-goto elements, submit the form directly
4242- let doQuickGoto = !isElemHidden($goto);
4343- const submitter = submitEventSubmitter(e.originalEvent);
4444- if (submitter !== $form[0] && submitter !== $input[0] && submitter !== $goto[0]) doQuickGoto = false;
4141+ let doQuickGoto = !isElemHidden(goto);
4242+ const submitter = submitEventSubmitter(e);
4343+ if (submitter !== form && submitter !== input && submitter !== goto) doQuickGoto = false;
4544 if (!doQuickGoto) return;
46454746 // if there is a goto button, use its link
4847 e.preventDefault();
4949- window.location.href = $goto.attr('data-issue-goto-link');
4848+ window.location.href = goto.getAttribute('data-issue-goto-link');
5049 });
51505251 const onInput = async () => {
5353- const searchText = $input.val();
5454-5252+ const searchText = input.value;
5553 // try to check whether the parsed goto link is valid
5654 let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
5755 if (targetUrl) {
5856 const res = await GET(`${targetUrl}/info`);
5957 if (res.status !== 200) targetUrl = '';
6058 }
6161-6259 // if the input value has changed, then ignore the result
6363- if ($input.val() !== searchText) return;
6060+ if (input.value !== searchText) return;
64616565- toggleElem($goto, Boolean(targetUrl));
6666- $goto.attr('data-issue-goto-link', targetUrl);
6262+ toggleElem(goto, Boolean(targetUrl));
6363+ goto.setAttribute('data-issue-goto-link', targetUrl);
6764 };
68656969- $input.on('input', onInputDebounce(onInput));
6666+ input.addEventListener('input', onInputDebounce(onInput));
7067 onInput();
7168}
+10-6
web_src/js/features/comp/ColorPicker.js
···22import {createColorPicker} from '../colorpicker.js';
3344export function initCompColorPicker() {
55- createColorPicker($('.color-picker'));
55+ (async () => {
66+ await createColorPicker(document.querySelectorAll('.color-picker'));
6777- $('.precolors .color').on('click', function () {
88- const color_hex = $(this).data('color-hex');
99- $('.color-picker').val(color_hex);
1010- $('.minicolors-swatch-color').css('background-color', color_hex);
1111- });
88+ for (const el of document.querySelectorAll('.precolors .color')) {
99+ el.addEventListener('click', (e) => {
1010+ const color = e.target.getAttribute('data-color-hex');
1111+ const parent = e.target.closest('.color.picker');
1212+ $(parent.querySelector('.color-picker')).minicolors('value', color);
1313+ });
1414+ }
1515+ })();
1216}