diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 048ca393d1be7..04eb02363452d 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -818,7 +818,7 @@ rules: unicorn/consistent-destructuring: [2] unicorn/consistent-empty-array-spread: [2] unicorn/consistent-existence-index-check: [0] - unicorn/consistent-function-scoping: [2] + unicorn/consistent-function-scoping: [0] unicorn/custom-error-definition: [0] unicorn/empty-brace-spaces: [2] unicorn/error-message: [0] diff --git a/modules/templates/helper.go b/modules/templates/helper.go index fdfb21925ab74..e262892069792 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -294,9 +294,7 @@ func timeEstimateString(timeSec any) string { return util.TimeEstimateString(v) } -type QueryString string - -func queryBuild(a ...any) QueryString { +func queryBuild(a ...any) template.URL { var s string if len(a)%2 == 1 { if v, ok := a[0].(string); ok { @@ -304,7 +302,7 @@ func queryBuild(a ...any) QueryString { panic("queryBuild: invalid argument") } s = v - } else if v, ok := a[0].(QueryString); ok { + } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { panic("queryBuild: invalid argument") @@ -356,7 +354,7 @@ func queryBuild(a ...any) QueryString { if s != "" && s != "&" && s[len(s)-1] == '&' { s = s[:len(s)-1] } - return QueryString(s) + return template.URL(s) } func panicIfDevOrTesting() { diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c7562c7f3bccf..c943c924c8f34 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 dc6ff2f481fe1..b46a8f75f30c9 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 231691b4a7774..237323a0fc42d 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 37e94aa802380..46e302d634a98 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/templates/projects/view.tmpl b/templates/projects/view.tmpl index 71f9d059adb9b..acaf45e8d28e9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -36,10 +36,11 @@ {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} - + {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 7335c949f4738..e686f1d60f6db 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -30,10 +30,11 @@ {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} -
+ {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index fb8f1a667e75c..9dd83ba188294 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -22,7 +22,7 @@ {{range $data.RepoLabels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} {{template "repo/issue/sidebar/label_list_item" dict "Label" .}} @@ -32,7 +32,7 @@ {{range $data.OrgLabels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} {{template "repo/issue/sidebar/label_list_item" dict "Label" .}} diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index fd8fac27e27b4..ca5b432804fe4 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -20,7 +20,7 @@ grid-row: 2; padding-left: 1em; } -.repo-home-sidebar-bottom > :first-child { +.repo-home-sidebar-bottom .flex-list > :first-child { border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */ } @@ -43,7 +43,7 @@ grid-row: 3; padding-left: 0; } - .repo-home-sidebar-bottom > :first-child { + .repo-home-sidebar-bottom .flex-list > :first-child { border-top: 0; } } diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index eecbf7ef55936..7f647b668a641 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 93b2042fa920a..62bfccd1393b2 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'; + +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; + +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 = {}; -const languagesByExt = {}; +const languagesByFilename: Record = {}; +const languagesByExt: Record = {}; -const baseOptions = { +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.ts b/web_src/js/features/repo-issue.ts index 477edbeb5f4c9..f5a36b7717e28 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -8,6 +8,7 @@ import {parseIssuePageInfo, 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 {fomanticQuery} from '../modules/fomantic/base.ts'; const {appSubUrl} = window.config; @@ -31,34 +32,35 @@ export function initRepoIssueSidebarList() { if (crossRepoSearch === 'true') { issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${issuePageInfo.repoId}&type=${issuePageInfo.issueDependencySearchType}`; } - $('#new-dependency-drop-list') - .dropdown({ - apiSettings: { - url: issueSearchUrl, - onResponse(response) { - const filteredResponse = {success: true, results: []}; - const currIssueId = $('#new-dependency-drop-list').data('issue-id'); - // Parse the response from the api to work with our dropdown - $.each(response, (_i, issue) => { - // Don't list current issue in the dependency list. - if (issue.id === currIssueId) { - return; - } - filteredResponse.results.push({ - name: `
#${issue.number} ${htmlEscape(issue.title)}
+ fomanticQuery('#new-dependency-drop-list').dropdown({ + fullTextSearch: true, + apiSettings: { + url: issueSearchUrl, + onResponse(response) { + const filteredResponse = {success: true, results: []}; + const currIssueId = $('#new-dependency-drop-list').data('issue-id'); + // Parse the response from the api to work with our dropdown + $.each(response, (_i, issue) => { + // Don't list current issue in the dependency list. + if (issue.id === currIssueId) { + return; + } + filteredResponse.results.push({ + name: `
#${issue.number} ${htmlEscape(issue.title)}
${htmlEscape(issue.repository.full_name)}
`, - value: issue.id, - }); + value: issue.id, }); - return filteredResponse; - }, - cache: false, + }); + return filteredResponse; }, + cache: false, + }, + }); +} - fullTextSearch: true, - }); - - $('.menu a.label-filter-item').each(function () { +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(); @@ -66,11 +68,9 @@ export function initRepoIssueSidebarList() { } }); }); - - // FIXME: it is wrong place to init ".ui.dropdown.label-filter" - $('.menu .ui.dropdown.label-filter').on('keydown', (e) => { + $('.ui.dropdown.label-filter').on('keydown', (e) => { if (e.altKey && e.key === 'Enter') { - const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected'); + const selectedItem = document.querySelector('.ui.dropdown.label-filter .menu .item.selected'); if (selectedItem) { excludeLabel(selectedItem); } diff --git a/web_src/js/index.ts b/web_src/js/index.ts index f93c3495af4c7..2964ef557200b 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -29,7 +29,7 @@ import { initRepoIssueWipTitle, initRepoPullRequestMergeInstruction, initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, initRepoIssueSidebarList, + initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueLabelFilter, } from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; @@ -181,6 +181,7 @@ onDomReady(() => { initRepoGraphGit, initRepoIssueContentHistory, initRepoIssueList, + initRepoIssueLabelFilter, initRepoIssueSidebarList, initRepoIssueReferenceRepositorySearch, initRepoIssueWipTitle, diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts new file mode 100644 index 0000000000000..587e0bca7c63f --- /dev/null +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -0,0 +1,56 @@ +import {createElementFromHTML} from '../../utils/dom.ts'; +import {hideScopedEmptyDividers} from './dropdown.ts'; + +test('hideScopedEmptyDividers-simple', () => { + const container = createElementFromHTML(`
+
+
a
+
+
+
+
b
+
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` + +
a
+ + +
+
b
+ +`); +}); + +test('hideScopedEmptyDividers-hidden1', () => { + const container = createElementFromHTML(`
+
a
+
+
b
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +
a
+ +
b
+`); +}); + +test('hideScopedEmptyDividers-hidden2', () => { + const container = createElementFromHTML(`
+
a
+
+
b
+
+
c
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +
a
+ +
b
+ +
c
+`); +}); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index d8fb4d6e6e41a..6d0f12cb43887 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -59,6 +59,12 @@ function updateSelectionLabel(label: HTMLElement) { } } +function processMenuItems($dropdown, dropdownCall) { + const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; + const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); + if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); +} + // delegate the dropdown's template functions and callback functions to add aria attributes. function delegateOne($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); @@ -72,6 +78,18 @@ function delegateOne($dropdown: any) { // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') }); + const oldFilterItems = dropdownCall('internal', 'filterItems'); + dropdownCall('internal', 'filterItems', function (...args: any[]) { + oldFilterItems.call(this, ...args); + processMenuItems($dropdown, dropdownCall); + }); + + const oldShow = dropdownCall('internal', 'show'); + dropdownCall('internal', 'show', function (...args: any[]) { + oldShow.call(this, ...args); + processMenuItems($dropdown, dropdownCall); + }); + // the "template" functions are used for dynamic creation (eg: AJAX) const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()}; const dropdownTemplatesMenuOld = dropdownTemplates.menu; @@ -271,3 +289,65 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT ignoreClickPreEvents = ignoreClickPreVisible = 0; }, true); } + +// Although Fomantic Dropdown supports "hideDividers", it doesn't really work with our "scoped dividers" +// At the moment, "label dropdown items" use scopes, a sample case is: +// * a-label +// * divider +// * scope/1 +// * scope/2 +// * divider +// * z-label +// when the "scope/*" are filtered out, we'd like to see "a-label" and "z-label" without the divider. +export function hideScopedEmptyDividers(container: Element) { + const visibleItems: Element[] = []; + const curScopeVisibleItems: Element[] = []; + let curScope: string = '', lastVisibleScope: string = ''; + const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope'); + const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items + + const handleScopeSwitch = (itemScope: string) => { + if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { + hideDivider(curScopeVisibleItems[0]); + } else if (curScopeVisibleItems.length) { + if (isScopedDivider(curScopeVisibleItems[0]) && lastVisibleScope === curScope) { + hideDivider(curScopeVisibleItems[0]); + curScopeVisibleItems.shift(); + } + visibleItems.push(...curScopeVisibleItems); + lastVisibleScope = curScope; + } + curScope = itemScope; + curScopeVisibleItems.length = 0; + }; + + // hide the scope dividers if the scope items are empty + for (const item of container.children) { + const itemScope = item.getAttribute('data-scope') || ''; + if (itemScope !== curScope) { + handleScopeSwitch(itemScope); + } + if (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) { + curScopeVisibleItems.push(item as HTMLElement); + } + } + handleScopeSwitch(''); + + // hide all leading and trailing dividers + while (visibleItems.length) { + if (!visibleItems[0].matches('.divider')) break; + hideDivider(visibleItems[0]); + visibleItems.shift(); + } + while (visibleItems.length) { + if (!visibleItems[visibleItems.length - 1].matches('.divider')) break; + hideDivider(visibleItems[visibleItems.length - 1]); + visibleItems.pop(); + } + // hide all duplicate dividers, hide current divider if next sibling is still divider + // no need to update "visibleItems" array since this is the last loop + for (const item of visibleItems) { + if (!item.matches('.divider')) continue; + if (item.nextElementSibling?.matches('.divider')) hideDivider(item); + } +} diff --git a/web_src/js/webcomponents/overflow-menu.ts b/web_src/js/webcomponents/overflow-menu.ts index 777d7dc65dd39..4e729a268a02f 100644 --- a/web_src/js/webcomponents/overflow-menu.ts +++ b/web_src/js/webcomponents/overflow-menu.ts @@ -12,7 +12,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { mutationObserver: MutationObserver; lastWidth: number; - updateItems = throttle(100, () => { // eslint-disable-line unicorn/consistent-function-scoping -- https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2088 + updateItems = throttle(100, () => { if (!this.tippyContent) { const div = document.createElement('div'); div.classList.add('tippy-target');