diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e262892069..ff9673ccef 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -42,7 +42,7 @@ func NewFuncMap() template.FuncMap { "HTMLFormat": htmlutil.HTMLFormat, "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, - "QueryBuild": queryBuild, + "QueryBuild": QueryBuild, "JSEscape": jsEscapeSafe, "SanitizeHTML": SanitizeHTML, "URLJoin": util.URLJoin, @@ -294,24 +294,27 @@ func timeEstimateString(timeSec any) string { return util.TimeEstimateString(v) } -func queryBuild(a ...any) template.URL { +// QueryBuild builds a query string from a list of key-value pairs. +// It omits the nil and empty strings, but it doesn't omit other zero values, +// because the zero value of number types may have a meaning. +func QueryBuild(a ...any) template.URL { var s string if len(a)%2 == 1 { if v, ok := a[0].(string); ok { if v == "" || (v[0] != '?' && v[0] != '&') { - panic("queryBuild: invalid argument") + panic("QueryBuild: invalid argument") } s = v } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { - panic("queryBuild: invalid argument") + panic("QueryBuild: invalid argument") } } for i := len(a) % 2; i < len(a); i += 2 { k, ok := a[i].(string) if !ok { - panic("queryBuild: invalid argument") + panic("QueryBuild: invalid argument") } var v string if va, ok := a[i+1].(string); ok { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 1c56dce822..f50ad1f298 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1109,7 +1109,7 @@ delete_preexisting_success = Deleted unadopted files in %s blame_prior = View blame prior to this change blame.ignore_revs = Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view. blame.ignore_revs.failed = Failed to ignore revisions in .git-blame-ignore-revs. -author_search_tooltip = Shows a maximum of 30 users +user_search_tooltip = Shows a maximum of 30 users tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s @@ -1529,7 +1529,8 @@ issues.filter_assignee = Assignee issues.filter_assginee_no_select = All assignees issues.filter_assginee_no_assignee = No assignee issues.filter_poster = Author -issues.filter_poster_no_select = All authors +issues.filter_user_placeholder = Search users +issues.filter_user_no_select = All users issues.filter_type = Type issues.filter_type.all_issues = All issues issues.filter_type.assigned_to_you = Assigned to you diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c7562c7f3b..c943c924c8 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -145,6 +145,7 @@ confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments name=Nom value=Valeur +readme=Lisez-moi filter=Filtrer filter.clear=Effacer le filtre @@ -1032,6 +1033,8 @@ fork_to_different_account=Créer une bifurcation vers un autre compte fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être modifiée. fork_branch=Branche à cloner sur la bifurcation all_branches=Toutes les branches +view_all_branches=Voir toutes les branches +view_all_tags=Voir toutes les étiquettes fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide. fork.blocked_user=Impossible de bifurquer le dépôt car vous êtes bloqué par son propriétaire. use_template=Utiliser ce modèle @@ -1043,6 +1046,8 @@ generate_repo=Générer un dépôt generate_from=Générer depuis repo_desc=Description repo_desc_helper=Décrire brièvement votre dépôt +repo_no_desc=Aucune description fournie +repo_lang=Langue repo_gitignore_helper=Sélectionner quelques .gitignore prédéfinies repo_gitignore_helper_desc=De nombreux outils et compilateurs génèrent des fichiers résiduels qui n'ont pas besoin d'être supervisés par git. Composez un .gitignore à l’aide de cette liste des languages de programmation courants. issue_labels=Jeu de labels pour les tickets @@ -1668,12 +1673,26 @@ issues.delete.title=Supprimer ce ticket ? issues.delete.text=Voulez-vous vraiment supprimer ce ticket ? (Cette opération supprimera définitivement tout le contenu. Envisagez plutôt de le fermer si vous avez l'intention de l'archiver) issues.tracker=Minuteur +issues.timetracker_timer_start=Démarrer le minuteur +issues.timetracker_timer_stop=Arrêter le minuteur +issues.timetracker_timer_discard=Annuler le minuteur +issues.timetracker_timer_manually_add=Pointer du temps +issues.time_estimate_placeholder=1h 2m +issues.time_estimate_set=Définir le temps estimé +issues.time_estimate_display=Estimation : %s +issues.change_time_estimate_at=a changé le temps estimé à %s %s +issues.remove_time_estimate_at=a supprimé le temps estimé %s +issues.time_estimate_invalid=Le format du temps estimé est invalide +issues.start_tracking_history=`a commencé son travail %s.` issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur un autre ticket !` +issues.stop_tracking_history=`a fini de travailler sur %s %s.` issues.cancel_tracking_history=`a abandonné son minuteur %s.` issues.del_time=Supprimer ce minuteur du journal +issues.add_time_history=`a pointé du temps de travail %s.` issues.del_time_history=`a supprimé son temps de travail %s.` +issues.add_time_manually=Temps pointé manuellement issues.add_time_hours=Heures issues.add_time_minutes=Minutes issues.add_time_sum_to_small=Aucun minuteur n'a été saisi. @@ -1926,6 +1945,10 @@ pulls.delete.title=Supprimer cette demande d'ajout ? pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.recently_pushed_new_branches=Vous avez soumis sur la branche %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s +pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s +pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements +pulls.upstream_diverging_merge=Synchroniser la bifurcation pull.deleted_branch=(supprimé) : %s pull.agit_documentation=Voir la documentation sur AGit @@ -3513,6 +3536,8 @@ alpine.repository=Informations sur le Dépôt alpine.repository.branches=Branches alpine.repository.repositories=Dépôts alpine.repository.architectures=Architectures +arch.registry=Ajouter un serveur avec un dépôt et une architecture liés dans /etc/pacman.conf : +arch.install=Synchroniser le paquet avec pacman : arch.repository=Informations sur le Dépôt arch.repository.repositories=Dépôts arch.repository.architectures=Architectures diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index dc6ff2f481..b46a8f75f3 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -145,6 +145,7 @@ confirm_delete_selected=Deimhnigh chun gach earra roghnaithe a scriosadh? name=Ainm value=Luach +readme=Readme filter=Scagaire filter.clear=Scagaire Soiléir @@ -1032,6 +1033,8 @@ fork_to_different_account=Forc chuig cuntas difriúil fork_visibility_helper=Ní féidir infheictheacht stór forcailte a athrú. fork_branch=Brainse le clónú chuig an bhforc all_branches=Gach brainse +view_all_branches=Féach ar gach brainse +view_all_tags=Féach ar gach clib fork_no_valid_owners=Ní féidir an stór seo a fhorcáil toisc nach bhfuil úinéirí bailí ann. fork.blocked_user=Ní féidir an stór a fhorcáil toisc go bhfuil úinéir an stórais bac ort. use_template=Úsáid an teimpléad seo @@ -1043,6 +1046,8 @@ generate_repo=Cruthaigh Stóras generate_from=Gin Ó repo_desc=Cur síos repo_desc_helper=Cuir isteach tuairisc ghearr (roghnach) +repo_no_desc=Níor tugadh tuairisc +repo_lang=Teangacha repo_gitignore_helper=Roghnaigh teimpléid .gitignore. repo_gitignore_helper_desc=Roghnaigh na comhaid nach bhfuil le rianú ó liosta teimpléid do theangacha coitianta. Cuirtear déantáin tipiciúla a ghineann uirlisí tógála gach teanga san áireamh ar.gitignore de réir réamhshocraithe. issue_labels=Lipéid Eisiúna @@ -1668,12 +1673,26 @@ issues.delete.title=Scrios an t-eagrán seo? issues.delete.text=An bhfuil tú cinnte gur mhaith leat an cheist seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) issues.tracker=Rianaitheoir Ama +issues.timetracker_timer_start=Amadóir tosaithe +issues.timetracker_timer_stop=Stop an t-amadóir +issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh +issues.timetracker_timer_manually_add=Cuir Am leis +issues.time_estimate_placeholder=1u 2n +issues.time_estimate_set=Socraigh am measta +issues.time_estimate_display=Meastachán: %s +issues.change_time_estimate_at=d'athraigh an meastachán ama go %s %s +issues.remove_time_estimate_at=baineadh meastachán ama %s +issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí +issues.start_tracking_history=thosaigh ag obair %s issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar eagrán eile!` +issues.stop_tracking_history=d'oibrigh do %s %s issues.cancel_tracking_history=`rianú ama curtha ar ceal %s` issues.del_time=Scrios an log ama seo +issues.add_time_history=cuireadh am caite %s %s leis issues.del_time_history=`an t-am caite scriosta %s` +issues.add_time_manually=Cuir Am leis de Láimh issues.add_time_hours=Uaireanta issues.add_time_minutes=Miontuairi issues.add_time_sum_to_small=Níor iontráilíodh aon am. @@ -1926,6 +1945,10 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo? pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %d tiomantas taobh thiar de %s +pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %d geallta taobh thiar de %s +pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s +pulls.upstream_diverging_merge=Forc sionc pull.deleted_branch=(scriosta): %s pull.agit_documentation=Déan athbhreithniú ar dhoiciméid faoi AGit @@ -3513,6 +3536,8 @@ alpine.repository=Eolas Stórais alpine.repository.branches=Brainsí alpine.repository.repositories=Stórais alpine.repository.architectures=Ailtireachtaí +arch.registry=Cuir freastalaí leis an stór agus an ailtireacht ghaolmhar le /etc/pacman.conf: +arch.install=Sioncronaigh pacáiste le pacman: arch.repository=Eolas Stórais arch.repository.repositories=Stórais arch.repository.architectures=Ailtireachtaí diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 231691b4a7..237323a0fc 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -76,29 +76,79 @@ loading=Memuat… +archived=Diarsipkan concept_code_repository=Repositori +show_full_screen=Tampilkan layar penuh +download_logs=Unduh Logs +confirm_delete_selected=Konfirmasi untuk menghapus semua item yang dipilih? name=Nama +value=Nilai +readme=Baca saya +filter=Saring +filter.clear=Hapus Filter +filter.is_archived=Diarsipkan +filter.not_archived=Tidak Diarsipkan filter.is_template=Contoh +filter.public=Publik filter.private=Pribadi +no_results_found=Hasil tidak ditemukan. [search] +search=Cari... +type_tooltip=Tipe pencarian +fuzzy_tooltip=Termasuk juga hasil yang mendekati kata pencarian +exact_tooltip=Hanya menampilkan hasil yang cocok dengan istilah pencarian +repo_kind=Cari repo... +user_kind=Telusuri pengguna... +org_kind=Cari organisasi... +team_kind=Cari tim... +code_kind=Cari kode... +code_search_unavailable=Pencarian kode saat ini tidak tersedia. Silahkan hubungi administrator. +branch_kind=Cari cabang... [aria] +navbar=Bar Navigasi +footer=Footer +footer.software=Tentang Software +footer.links=Tautan [heatmap] +number_of_contributions_in_the_last_12_months=%s Kontribusi pada 12 bulan terakhir +no_contributions=Belum ada kontribusi +less=Lebih sedikit +more=Lebih banyak [editor] +buttons.heading.tooltip=Tambahkan heading +buttons.bold.tooltip=Tambahkan teks Tebal +buttons.italic.tooltip=Tambahkan teks Miring +buttons.quote.tooltip=Kutip teks +buttons.code.tooltip=Tambah Kode +buttons.link.tooltip=Tambahkan tautan +buttons.list.unordered.tooltip=Tambah daftar titik +buttons.list.ordered.tooltip=Tambah daftar angka +buttons.list.task.tooltip=Tambahkan daftar tugas buttons.table.add.insert=Tambah +buttons.mention.tooltip=Tandai pengguna atau tim +buttons.ref.tooltip=Merujuk pada isu atau permintaan tarik +buttons.switch_to_legacy.tooltip=Gunakan editor versi lama +buttons.enable_monospace_font=Aktifkan font monospace +buttons.disable_monospace_font=Non-Aktifkan font monospace [filter] +string.asc=A - Z +string.desc=Z - A [error] +occurred=Terjadi kesalahan +report_message=Jika Anda yakin ini adalah bug Gitea, silakan cari isu di GitHub atau buka isu baru jika diperlukan. +not_found=Target tidak dapat ditemukan. [startpage] app_desc=Sebuah layanan hosting Git sendiri yang tanpa kesulitan @@ -118,8 +168,10 @@ path=Jalur repo_path=Jalur akar repositori +email_title=Pengaturan email smtp_addr=Host SMTP smtp_port=Port SMTP +smtp_from=Kirim Email Sebagai register_confirm=Perlu Konfirmasi Email Saat Pendaftaran mail_notify=Aktifkan Notifikasi Email disable_gravatar=Menonaktifkan Gravatar @@ -140,6 +192,7 @@ my_orgs=Organisasi Saya my_mirrors=Duplikat Saya view_home=Lihat %s +show_archived=Diarsipkan show_private=Pribadi @@ -481,6 +534,7 @@ email_notifications.enable=Aktifkan Pemberitahuan Surel email_notifications.disable=Nonaktifkan Email Notifikasi email_notifications.submit=Pasang Pengaturan Email +visibility.public=Publik visibility.private=Pribadi [repo] @@ -522,7 +576,9 @@ delete_preexisting_label=Hapus desc.private=Pribadi +desc.public=Publik desc.template=Contoh +desc.archived=Diarsipkan template.webhooks=Webhooks template.topics=Topik @@ -947,6 +1003,7 @@ settings=Pengaturan settings.full_name=Nama Lengkap settings.website=Situs web settings.location=Lokasi +settings.visibility.public=Publik settings.visibility.private_shortname=Pribadi settings.update_settings=Perbarui Setelan @@ -1033,6 +1090,7 @@ users.created=Dibuat users.edit=Edit users.auth_source=Sumber Otentikasi users.local=Lokal +users.list_status_filter.menu_text=Saring users.list_status_filter.is_admin=Pengelola emails.activated=Diaktifkan diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 37e94aa802..46e302d634 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -26,9 +26,9 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte "::endgroup::", "message for: step={step}, cursor={cursor}", "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", + "##[group]test group for: step={step}, cursor={cursor}", + "in group msg for: step={step}, cursor={cursor}", + "##[endgroup]", } cur := logCur.Cursor // usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally for i := 0; i < util.Iif(logCur.Step == 0, 3, 1); i++ { @@ -52,6 +52,10 @@ func MockActionsRunsJobs(ctx *context.Context) { req := web.GetForm(ctx).(*actions.ViewRequest) resp := &actions.ViewResponse{} + resp.State.Run.TitleHTML = `mock run title link` + resp.State.Run.Status = actions_model.StatusRunning.String() + resp.State.Run.CanCancel = true + resp.State.Run.CanDeleteArtifact = true resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 6dfefbf68d..b94344f2ec 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -339,12 +339,7 @@ func ViewProject(ctx *context.Context) { // 0 means issues with no label // blank means labels will not be filtered for issues selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true - } - if len(selectLabels) > 0 { + if selectLabels != "" { labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) if err != nil { ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 2123d4a5b6..6451f7ac76 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "net/http" - "net/url" "strconv" "strings" @@ -531,12 +530,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt // 0 means issues with no label // blank means labels will not be filtered for issues selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true - } - if len(selectLabels) > 0 { + if selectLabels != "" { labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) if err != nil { ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) @@ -616,8 +610,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["TotalTrackedTime"] = totalTrackedTime } - archived := ctx.FormBool("archived") - page := ctx.FormInt("page") if page <= 1 { page = 1 @@ -792,21 +784,13 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt return } + showArchivedLabels := ctx.FormBool("archived_labels") + ctx.Data["ShowArchivedLabels"] = showArchivedLabels ctx.Data["PinnedIssues"] = pinned ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["IssueStats"] = issueStats ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["ClosedCount"] = issueStats.ClosedCount - linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%v&archived=%t" - ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) - ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) - ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, url.QueryEscape(posterUsername), archived) ctx.Data["SelLabelIDs"] = labelIDs ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType @@ -814,6 +798,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["MilestoneID"] = milestoneID ctx.Data["ProjectID"] = projectID ctx.Data["AssigneeID"] = assigneeID + ctx.Data["PosterUserID"] = posterUserID ctx.Data["PosterUsername"] = posterUsername ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed @@ -825,7 +810,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt default: ctx.Data["State"] = "open" } - ctx.Data["ShowArchivedLabels"] = archived pager.AddParamString("q", keyword) pager.AddParamString("type", viewType) @@ -836,8 +820,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt pager.AddParamString("project", fmt.Sprint(projectID)) pager.AddParamString("assignee", fmt.Sprint(assigneeID)) pager.AddParamString("poster", posterUsername) - pager.AddParamString("archived", fmt.Sprint(archived)) - + if showArchivedLabels { + pager.AddParamString("archived_labels", "true") + } ctx.Data["Page"] = pager } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 3afdcfad8b..33c15e7767 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -66,12 +66,6 @@ func Milestones(ctx *context.Context) { } ctx.Data["OpenCount"] = stats.OpenCount ctx.Data["ClosedCount"] = stats.ClosedCount - linkStr := "%s/milestones?state=%s&q=%s&sort=%s" - ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Repo.RepoLink, "open", - url.QueryEscape(keyword), url.QueryEscape(sortType)) - ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Repo.RepoLink, "closed", - url.QueryEscape(keyword), url.QueryEscape(sortType)) - if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { if err := issues_model.MilestoneList(miles).LoadTotalTrackedTimes(ctx); err != nil { ctx.ServerError("LoadTotalTrackedTimes", err) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 95ae84ab93..168da2ca1f 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -312,12 +312,7 @@ func ViewProject(ctx *context.Context) { // 0 means issues with no label // blank means labels will not be filtered for issues selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true - } - if len(selectLabels) > 0 { + if selectLabels != "" { labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) if err != nil { ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index acaf45e8d2..966d3bf604 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -5,79 +5,19 @@

{{.Project.Title}}

{{if $canWriteProject}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl new file mode 100644 index 0000000000..67bfab6fb0 --- /dev/null +++ b/templates/repo/issue/filter_item_label.tmpl @@ -0,0 +1,45 @@ +{{/* +* "labels" from query string (needed by JS) +* QueryLink +* Labels +* SupportArchivedLabel, if true, then it needs "archived_labels" from query string +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl new file mode 100644 index 0000000000..4f1db71d57 --- /dev/null +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -0,0 +1,31 @@ +{{/* This is a user list for filter, the data is provided by a local variable assignment +* QueryParamKey: eg: "poster", "assignee" +* QueryLink +* UserSearchList +* SelectedUserId: 0 or empty means default, -1 means "no user is set" +* TextFilterTitle +* TextZeroValue: the text for "all issues" +* TextNegativeOne: the text for "issues with no assignee" +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_item_user_fetch.tmpl b/templates/repo/issue/filter_item_user_fetch.tmpl new file mode 100644 index 0000000000..cab128a787 --- /dev/null +++ b/templates/repo/issue/filter_item_user_fetch.tmpl @@ -0,0 +1,23 @@ +{{/* This is a user list for filter, the data is provided by a remote "fetch" request +* QueryParamKey: eg: "poster", "assignee" +* QueryLink +* UserSearchUrl +* SelectedUserId +* TextFilterTitle +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index e686f1d60f..c78d23d51c 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -1,55 +1,6 @@ -{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived" (Iif $.ShowArchivedLabels NIL)}} - - +{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}} + +{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}} {{if not .Milestone}} @@ -128,46 +79,24 @@ - - +{{/* TODO: the UserSearchUrl is old logic but not right, milestone could also have "pull request" posters */}} +{{template "repo/issue/filter_item_user_fetch" dict + "QueryParamKey" "poster" + "QueryLink" $queryLink + "UserSearchUrl" (Iif .Milestone (print $.RepoLink "/issues/posters") (print $.Link "/posters")) + "SelectedUserId" $.PosterUserID + "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster") +}} - - +{{template "repo/issue/filter_item_user_assign" dict + "QueryParamKey" "assignee" + "QueryLink" $queryLink + "UserSearchList" $.Assignees + "SelectedUserId" $.AssigneeID + "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") + "TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") + "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") +}} {{if .IsSigned}} diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl index eb2d6e09ee..b9dd04a7db 100644 --- a/templates/repo/issue/openclose.tmpl +++ b/templates/repo/issue/openclose.tmpl @@ -1,16 +1,23 @@ +{{/* this tmpl is quite dirty, it should not mix unrelated things together .... need to split it in the future*/}} +{{$allStatesLink := ""}}{{$openLink := ""}}{{$closedLink := ""}} +{{if .PageIsMilestones}} + {{$allStatesLink = QueryBuild "?" "q" $.Keyword "sort" $.SortType "state" "all"}} +{{else}} + {{$allStatesLink = QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" "all" "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}} +{{end}} +{{$openLink = QueryBuild $allStatesLink "state" "open"}} +{{$closedLink = QueryBuild $allStatesLink "state" "closed"}} diff --git a/tsconfig.json b/tsconfig.json index 744f1511e9..e006535c02 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,7 @@ ], "compilerOptions": { "target": "es2020", - "module": "node16", - "moduleResolution": "node16", + "module": "nodenext", "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], "allowImportingTsExtensions": true, "allowJs": true, diff --git a/web_src/css/base.css b/web_src/css/base.css index 0f01f18e28..f01f1bedc7 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1401,8 +1401,9 @@ table th[data-sortt-desc] .svg { min-width: 0; } -/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content */ -.ui.dropdown .menu.flex-items-menu > .item { +/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content +the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */ +.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { display: flex !important; align-items: center; gap: .5rem; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 14bdc43474..9e1def87a7 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -74,24 +74,6 @@ } } -.repository .filter.menu.labels .label-filter .menu .info { - display: inline-block; - padding: 0.5rem 0; - font-size: 12px; - width: 100%; - white-space: nowrap; - margin-left: 10px; - margin-right: 8px; - text-align: left; -} - -.repository .filter.menu.labels .label-filter .menu .info code { - border: 1px solid var(--color-secondary); - border-radius: var(--border-radius); - padding: 1px 2px; - font-size: 11px; -} - /* make all issue filter dropdown menus popup leftward, to avoid go out the viewport (right side) */ .repository .filter.menu .ui.dropdown .menu { max-height: 500px; @@ -108,6 +90,24 @@ left: 0; } +.repository .filter.menu .ui.dropdown.label-filter .menu .info { + display: inline-block; + padding: 0.5rem 0; + font-size: 12px; + width: 100%; + white-space: nowrap; + margin-left: 10px; + margin-right: 8px; + text-align: left; +} + +.repository .filter.menu .ui.dropdown.label-filter .menu .info code { + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + padding: 1px 2px; + font-size: 11px; +} + /* For the secondary pointing menu, respect its own border-bottom */ /* style reference: https://semantic-ui.com/collections/menu.html#pointing */ .repository .ui.tabs.container .ui.menu:not(.secondary.pointing) { diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1e0f82ce27..4fafc7d6f8 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -68,10 +68,8 @@ background-color: var(--color-secondary-dark-4); } -.archived-label-filter { - margin-left: 10px; +.label-filter-archived-toggle { + margin: 8px 10px; font-size: 12px; - display: flex !important; - margin-bottom: 8px; min-width: fit-content; } diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index eecbf7ef55..7f647b668a 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -16,8 +16,27 @@ type LogLine = { message: string; }; -const LogLinePrefixGroup = '::group::'; -const LogLinePrefixEndGroup = '::endgroup::'; +const LogLinePrefixesGroup = ['::group::', '##[group]']; +const LogLinePrefixesEndGroup = ['::endgroup::', '##[endgroup]']; + +type LogLineCommand = { + name: 'group' | 'endgroup', + prefix: string, +} + +function parseLineCommand(line: LogLine): LogLineCommand | null { + for (const prefix of LogLinePrefixesGroup) { + if (line.message.startsWith(prefix)) { + return {name: 'group', prefix}; + } + } + for (const prefix of LogLinePrefixesEndGroup) { + if (line.message.startsWith(prefix)) { + return {name: 'endgroup', prefix}; + } + } + return null; +} const sfc = { name: 'RepoActionView', @@ -129,13 +148,13 @@ const sfc = { return el._stepLogsActiveContainer ?? el; }, // begin a log group - beginLogGroup(stepIndex: number, startTime: number, line: LogLine) { + beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { const el = this.$refs.logs[stepIndex]; const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'}, this.createLogLine(stepIndex, startTime, { index: line.index, timestamp: line.timestamp, - message: line.message.substring(LogLinePrefixGroup.length), + message: line.message.substring(cmd.prefix.length), }), ); const elJobLogList = createElementFromAttrs('div', {class: 'job-log-list'}); @@ -147,13 +166,13 @@ const sfc = { el._stepLogsActiveContainer = elJobLogList; }, // end a log group - endLogGroup(stepIndex: number, startTime: number, line: LogLine) { + endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { const el = this.$refs.logs[stepIndex]; el._stepLogsActiveContainer = null; el.append(this.createLogLine(stepIndex, startTime, { index: line.index, timestamp: line.timestamp, - message: line.message.substring(LogLinePrefixEndGroup.length), + message: line.message.substring(cmd.prefix.length), })); }, @@ -201,11 +220,12 @@ const sfc = { appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) { for (const line of logLines) { const el = this.getLogsContainer(stepIndex); - if (line.message.startsWith(LogLinePrefixGroup)) { - this.beginLogGroup(stepIndex, startTime, line); + const cmd = parseLineCommand(line); + if (cmd?.name === 'group') { + this.beginLogGroup(stepIndex, startTime, line, cmd); continue; - } else if (line.message.startsWith(LogLinePrefixEndGroup)) { - this.endLogGroup(stepIndex, startTime, line); + } else if (cmd?.name === 'endgroup') { + this.endLogGroup(stepIndex, startTime, line, cmd); continue; } el.append(this.createLogLine(stepIndex, startTime, line)); @@ -393,7 +413,7 @@ export function initRepositoryActionView() { - @@ -539,6 +559,11 @@ export function initRepositoryActionView() { overflow-wrap: anywhere; } +.action-info-summary .ui.button { + margin: 0; + white-space: nowrap; +} + .action-commit-summary { display: flex; flex-wrap: wrap; diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 93b2042fa9..62bfccd139 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -1,11 +1,30 @@ import tinycolor from 'tinycolor2'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; import {onInputDebounce} from '../utils/dom.ts'; +import type MonacoNamespace from 'monaco-editor'; -const languagesByFilename = {}; -const languagesByExt = {}; +type Monaco = typeof MonacoNamespace; +type IStandaloneCodeEditor = MonacoNamespace.editor.IStandaloneCodeEditor; +type IEditorOptions = MonacoNamespace.editor.IEditorOptions; +type IGlobalEditorOptions = MonacoNamespace.editor.IGlobalEditorOptions; +type ITextModelUpdateOptions = MonacoNamespace.editor.ITextModelUpdateOptions; +type MonacoOpts = IEditorOptions & IGlobalEditorOptions & ITextModelUpdateOptions; -const baseOptions = { +type EditorConfig = { + indent_style?: 'tab' | 'space', + indent_size?: string | number, // backend emits this as string + tab_width?: string | number, // backend emits this as string + end_of_line?: 'lf' | 'cr' | 'crlf', + charset?: 'latin1' | 'utf-8' | 'utf-8-bom' | 'utf-16be' | 'utf-16le', + trim_trailing_whitespace?: boolean, + insert_final_newline?: boolean, + root?: boolean, +} + +const languagesByFilename: Record = {}; +const languagesByExt: Record = {}; + +const baseOptions: MonacoOpts = { fontFamily: 'var(--fonts-monospace)', fontSize: 14, // https://github.com/microsoft/monaco-editor/issues/2242 guides: {bracketPairs: false, indentation: false}, @@ -15,21 +34,23 @@ const baseOptions = { overviewRulerLanes: 0, renderLineHighlight: 'all', renderLineHighlightOnlyWhenFocus: true, - rulers: false, + rulers: [], scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6}, scrollBeyondLastLine: false, automaticLayout: true, }; -function getEditorconfig(input: HTMLInputElement) { +function getEditorconfig(input: HTMLInputElement): EditorConfig | null { + const json = input.getAttribute('data-editorconfig'); + if (!json) return null; try { - return JSON.parse(input.getAttribute('data-editorconfig')); + return JSON.parse(json); } catch { return null; } } -function initLanguages(monaco) { +function initLanguages(monaco: Monaco): void { for (const {filenames, extensions, id} of monaco.languages.getLanguages()) { for (const filename of filenames || []) { languagesByFilename[filename] = id; @@ -40,35 +61,26 @@ function initLanguages(monaco) { } } -function getLanguage(filename) { +function getLanguage(filename: string): string { return languagesByFilename[filename] || languagesByExt[extname(filename)] || 'plaintext'; } -function updateEditor(monaco, editor, filename, lineWrapExts) { +function updateEditor(monaco: Monaco, editor: IStandaloneCodeEditor, filename: string, lineWrapExts: string[]): void { editor.updateOptions(getFileBasedOptions(filename, lineWrapExts)); const model = editor.getModel(); + if (!model) return; const language = model.getLanguageId(); const newLanguage = getLanguage(filename); if (language !== newLanguage) monaco.editor.setModelLanguage(model, newLanguage); } // export editor for customization - https://github.com/go-gitea/gitea/issues/10409 -function exportEditor(editor) { +function exportEditor(editor: IStandaloneCodeEditor): void { if (!window.codeEditors) window.codeEditors = []; if (!window.codeEditors.includes(editor)) window.codeEditors.push(editor); } -export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, editorOpts: Record) { - const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); - - initLanguages(monaco); - let {language, ...other} = editorOpts; - if (!language) language = getLanguage(filename); - - const container = document.createElement('div'); - container.className = 'monaco-editor-container'; - textarea.parentNode.append(container); - +function updateTheme(monaco: Monaco): void { // https://github.com/microsoft/monaco-editor/issues/2427 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format const styles = window.getComputedStyle(document.documentElement); @@ -80,6 +92,7 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri rules: [ { background: getColor('--color-code-bg'), + token: '', }, ], colors: { @@ -101,6 +114,26 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri 'focusBorder': '#0000', // prevent blue border }, }); +} + +type CreateMonacoOpts = MonacoOpts & {language?: string}; + +export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> { + const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); + + initLanguages(monaco); + let {language, ...other} = opts; + if (!language) language = getLanguage(filename); + + const container = document.createElement('div'); + container.className = 'monaco-editor-container'; + if (!textarea.parentNode) throw new Error('Parent node absent'); + textarea.parentNode.append(container); + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + updateTheme(monaco); + }); + updateTheme(monaco); const editor = monaco.editor.create(container, { value: textarea.value, @@ -114,8 +147,12 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri ]); const model = editor.getModel(); + if (!model) throw new Error('Unable to get editor model'); model.onDidChangeContent(() => { - textarea.value = editor.getValue({preserveBOM: true}); + textarea.value = editor.getValue({ + preserveBOM: true, + lineEnding: '', + }); textarea.dispatchEvent(new Event('change')); // seems to be needed for jquery-are-you-sure }); @@ -127,13 +164,13 @@ export async function createMonaco(textarea: HTMLTextAreaElement, filename: stri return {monaco, editor}; } -function getFileBasedOptions(filename: string, lineWrapExts: string[]) { +function getFileBasedOptions(filename: string, lineWrapExts: string[]): MonacoOpts { return { wordWrap: (lineWrapExts || []).includes(extname(filename)) ? 'on' : 'off', }; } -function togglePreviewDisplay(previewable: boolean) { +function togglePreviewDisplay(previewable: boolean): void { const previewTab = document.querySelector('a[data-tab="preview"]'); if (!previewTab) return; @@ -145,19 +182,19 @@ function togglePreviewDisplay(previewable: boolean) { // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active if (previewTab.classList.contains('active')) { const writeTab = document.querySelector('a[data-tab="write"]'); - writeTab.click(); + writeTab?.click(); } } } -export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement) { +export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameInput: HTMLInputElement): Promise { const filename = basename(filenameInput.value); const previewableExts = new Set((textarea.getAttribute('data-previewable-extensions') || '').split(',')); const lineWrapExts = (textarea.getAttribute('data-line-wrap-extensions') || '').split(','); - const previewable = previewableExts.has(extname(filename)); + const isPreviewable = previewableExts.has(extname(filename)); const editorConfig = getEditorconfig(filenameInput); - togglePreviewDisplay(previewable); + togglePreviewDisplay(isPreviewable); const {monaco, editor} = await createMonaco(textarea, filename, { ...baseOptions, @@ -175,14 +212,22 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn return editor; } -function getEditorConfigOptions(ec: Record): Record { - if (!isObject(ec)) return {}; +function getEditorConfigOptions(ec: EditorConfig | null): MonacoOpts { + if (!ec || !isObject(ec)) return {}; - const opts: Record = {}; + const opts: MonacoOpts = {}; opts.detectIndentation = !('indent_style' in ec) || !('indent_size' in ec); - if ('indent_size' in ec) opts.indentSize = Number(ec.indent_size); - if ('tab_width' in ec) opts.tabSize = Number(ec.tab_width) || opts.indentSize; - if ('max_line_length' in ec) opts.rulers = [Number(ec.max_line_length)]; + + if ('indent_size' in ec) { + opts.indentSize = Number(ec.indent_size); + } + if ('tab_width' in ec) { + opts.tabSize = Number(ec.tab_width) || Number(ec.indent_size); + } + if ('max_line_length' in ec) { + opts.rulers = [Number(ec.max_line_length)]; + } + opts.trimAutoWhitespace = ec.trim_trailing_whitespace === true; opts.insertSpaces = ec.indent_style === 'space'; opts.useTabStops = ec.indent_style === 'tab'; diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 48e22ba3c9..a0550837ec 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -1,5 +1,5 @@ import {updateIssuesMeta} from './repo-common.ts'; -import {toggleElem, hideElem, isElemHidden, queryElems} from '../utils/dom.ts'; +import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {confirmModal} from './comp/ConfirmModal.ts'; import {showErrorToast} from '../modules/toast.ts'; @@ -95,34 +95,51 @@ function initRepoIssueListCheckboxes() { function initDropdownUserRemoteSearch(el: Element) { let searchUrl = el.getAttribute('data-search-url'); const actionJumpUrl = el.getAttribute('data-action-jump-url'); - const selectedUserId = el.getAttribute('data-selected-user-id'); + const selectedUserId = parseInt(el.getAttribute('data-selected-user-id')); + let selectedUsername = ''; if (!searchUrl.includes('?')) searchUrl += '?'; const $searchDropdown = fomanticQuery(el); + const elSearchInput = el.querySelector('.ui.search input'); + const elItemFromInput = el.querySelector('.menu > .item-from-input'); + $searchDropdown.dropdown('setting', { fullTextSearch: true, selectOnKeydown: false, - apiSettings: { + action: (_text, value) => { + window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); + }, + }); + + type ProcessedResult = {value: string, name: string}; + const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items + const syncItemFromInput = () => { + elItemFromInput.setAttribute('data-value', elSearchInput.value); + elItemFromInput.textContent = elSearchInput.value; + toggleElem(elItemFromInput, !processedResults.length); + }; + + if (!searchUrl) { + elSearchInput.addEventListener('input', syncItemFromInput); + } else { + $searchDropdown.dropdown('setting', 'apiSettings', { cache: false, url: `${searchUrl}&q={query}`, onResponse(resp) { // the content is provided by backend IssuePosters handler - const processedResults = []; // to be used by dropdown to generate menu items + processedResults.length = 0; for (const item of resp.results) { let html = `${htmlEscape(item.username)}`; if (item.full_name) html += `${htmlEscape(item.full_name)}`; + if (selectedUserId === item.user_id) selectedUsername = item.username; processedResults.push({value: item.username, name: html}); } resp.results = processedResults; + syncItemFromInput(); return resp; }, - }, - action: (_text, value) => { - window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); - }, - onShow: () => { - $searchDropdown.dropdown('filter', ' '); // trigger a search on first show - }, - }); + }); + $searchDropdown.dropdown('setting', 'onShow', () => $searchDropdown.dropdown('filter', ' ')); // trigger a search on first show + } // we want to generate the dropdown menu items by ourselves, replace its internal setup functions const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; @@ -151,7 +168,7 @@ function initDropdownUserRemoteSearch(el: Element) { for (const el of menu.querySelectorAll('.item.active, .item.selected')) { el.classList.remove('active', 'selected'); } - menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected'); + menu.querySelector(`.item[data-value="${CSS.escape(selectedUsername)}"]`)?.classList.add('selected'); }, 0); }; } @@ -203,44 +220,9 @@ async function initIssuePinSort() { }); } -function initArchivedLabelFilter() { - const archivedLabelEl = document.querySelector('#archived-filter-checkbox'); - if (!archivedLabelEl) return; - - const url = new URL(window.location.href); - const archivedLabels = document.querySelectorAll('[data-is-archived]'); - - if (!archivedLabels.length) { - hideElem('.archived-label-filter'); - return; - } - const selectedLabels = (url.searchParams.get('labels') || '') - .split(',') - .map((id) => parseInt(id) < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve - - const archivedElToggle = () => { - for (const label of archivedLabels) { - const id = label.getAttribute('data-label-id'); - toggleElem(label, archivedLabelEl.checked || selectedLabels.includes(id)); - } - }; - - archivedElToggle(); - archivedLabelEl.addEventListener('change', () => { - archivedElToggle(); - if (archivedLabelEl.checked) { - url.searchParams.set('archived', 'true'); - } else { - url.searchParams.delete('archived'); - } - window.location.href = url.href; - }); -} - export function initRepoIssueList() { if (!document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) return; initRepoIssueListCheckboxes(); queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el)); initIssuePinSort(); - initArchivedLabelFilter(); } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index f5a36b7717..e4f9ce4cde 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -1,7 +1,14 @@ import $ from 'jquery'; import {htmlEscape} from 'escape-goat'; import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts'; -import {addDelegatedEventListener, createElementFromHTML, hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import { + addDelegatedEventListener, + createElementFromHTML, + hideElem, + queryElems, + showElem, + toggleElem, +} from '../utils/dom.ts'; import {setFileFolding} from './file-fold.ts'; import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts'; @@ -12,19 +19,6 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; const {appSubUrl} = window.config; -/** - * @param {HTMLElement} item - */ -function excludeLabel(item) { - const href = item.getAttribute('href'); - const id = item.getAttribute('data-label-id'); - - const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`; - const newStr = 'labels=$1-$2$3&'; - - window.location.assign(href.replace(new RegExp(regStr), newStr)); -} - export function initRepoIssueSidebarList() { const issuePageInfo = parseIssuePageInfo(); const crossRepoSearch = $('#crossRepoSearch').val(); @@ -58,24 +52,74 @@ export function initRepoIssueSidebarList() { }); } -export function initRepoIssueLabelFilter() { - // the "label-filter" is used in 2 templates: projects/view, issue/filter_list (issue list page including the milestone page) - $('.ui.dropdown.label-filter a.label-filter-item').each(function () { - $(this).on('click', function (e) { - if (e.altKey) { - e.preventDefault(); - excludeLabel(this); - } +function initRepoIssueLabelFilter(elDropdown: Element) { + const url = new URL(window.location.href); + const showArchivedLabels = url.searchParams.get('archived_labels') === 'true'; + const queryLabels = url.searchParams.get('labels') || ''; + const selectedLabelIds = new Set(); + for (const id of queryLabels ? queryLabels.split(',') : []) { + selectedLabelIds.add(`${Math.abs(parseInt(id))}`); // "labels" contains negative ids, which are excluded + } + + const excludeLabel = (e: MouseEvent|KeyboardEvent, item: Element) => { + e.preventDefault(); + e.stopPropagation(); + const labelId = item.getAttribute('data-label-id'); + let labelIds: string[] = queryLabels ? queryLabels.split(',') : []; + labelIds = labelIds.filter((id) => Math.abs(parseInt(id)) !== Math.abs(parseInt(labelId))); + labelIds.push(`-${labelId}`); + url.searchParams.set('labels', labelIds.join(',')); + window.location.assign(url); + }; + + // alt(or option) + click to exclude label + queryElems(elDropdown, '.label-filter-query-item', (el) => { + el.addEventListener('click', (e: MouseEvent) => { + if (e.altKey) excludeLabel(e, el); }); }); - $('.ui.dropdown.label-filter').on('keydown', (e) => { + // alt(or option) + enter to exclude selected label + elDropdown.addEventListener('keydown', (e: KeyboardEvent) => { if (e.altKey && e.key === 'Enter') { - const selectedItem = document.querySelector('.ui.dropdown.label-filter .menu .item.selected'); - if (selectedItem) { - excludeLabel(selectedItem); - } + const selectedItem = elDropdown.querySelector('.label-filter-query-item.selected'); + if (selectedItem) excludeLabel(e, selectedItem); } }); + // no "labels" query parameter means "all issues" + elDropdown.querySelector('.label-filter-query-default').classList.toggle('selected', queryLabels === ''); + // "labels=0" query parameter means "issues without label" + elDropdown.querySelector('.label-filter-query-not-set').classList.toggle('selected', queryLabels === '0'); + + // prepare to process "archived" labels + const elShowArchivedLabel = elDropdown.querySelector('.label-filter-archived-toggle'); + if (!elShowArchivedLabel) return; + const elShowArchivedInput = elShowArchivedLabel.querySelector('input'); + elShowArchivedInput.checked = showArchivedLabels; + const archivedLabels = elDropdown.querySelectorAll('.item[data-is-archived]'); + // if no archived labels, hide the toggle and return + if (!archivedLabels.length) { + hideElem(elShowArchivedLabel); + return; + } + + // show the archived labels if the toggle is checked or the label is selected + for (const label of archivedLabels) { + toggleElem(label, showArchivedLabels || selectedLabelIds.has(label.getAttribute('data-label-id'))); + } + // update the url when the toggle is changed and reload + elShowArchivedInput.addEventListener('input', () => { + if (elShowArchivedInput.checked) { + url.searchParams.set('archived_labels', 'true'); + } else { + url.searchParams.delete('archived_labels'); + } + window.location.assign(url); + }); +} + +export function initRepoIssueFilterItemLabel() { + // the "label-filter" is used in 2 templates: projects/view, issue/filter_list (issue list page including the milestone page) + queryElems(document, '.ui.dropdown.label-filter', initRepoIssueLabelFilter); } export function initRepoIssueCommentDelete() { diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2964ef5572..51d8c96fbd 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -29,7 +29,7 @@ import { initRepoIssueWipTitle, initRepoPullRequestMergeInstruction, initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueLabelFilter, + initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueFilterItemLabel, } from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; @@ -181,7 +181,7 @@ onDomReady(() => { initRepoGraphGit, initRepoIssueContentHistory, initRepoIssueList, - initRepoIssueLabelFilter, + initRepoIssueFilterItemLabel, initRepoIssueSidebarList, initRepoIssueReferenceRepositorySearch, initRepoIssueWipTitle, diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts index 274c9322ff..af669116a2 100644 --- a/web_src/js/utils/match.ts +++ b/web_src/js/utils/match.ts @@ -1,4 +1,4 @@ -import emojis from '../../../assets/emoji.json'; +import emojis from '../../../assets/emoji.json' with {type: 'json'}; import {GET} from '../modules/fetch.ts'; import type {Issue} from '../types.ts';