From 0d006290a7b8c96e544d4d73b8bf7a5b2047bbf4 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Feb 2026 11:50:44 +0100 Subject: [PATCH 01/50] Inline and lazy-load EasyMDE CSS, fix border colors (#36714) Replace the external easymde.min.css import with an inlined and lazy-loaded CSS file that uses proper theme variables for border colors. All EasyMDE/CodeMirror rules are scoped under `.EasyMDEContainer`, removing the need for !important overrides. - Fixes easymde borders, these were broken since a while now - Scope all easymde styles to .EasyMDEContainer - Inline easymde.min.css and codemirror.css into web_src/css/easymde.css - Lazy-load the CSS alongside the JS in switchToEasyMDE() - Fix .editor-toolbar and .CodeMirror border colors to use --color-input-border matching textarea inputs - Remove unused gutter, line number, and other unconfigured styles - Move .editor-loading to codeeditor.css where it belongs image --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- web_src/css/codemirror/base.css | 49 -- web_src/css/codemirror/dark.css | 88 ++-- web_src/css/easymde.css | 442 ++++++++++++++++++ web_src/css/editor/fileeditor.css | 56 --- web_src/css/features/codeeditor.css | 5 + web_src/css/index.css | 2 - .../js/features/comp/ComboMarkdownEditor.ts | 6 +- web_src/js/index-domready.ts | 1 - 8 files changed, 495 insertions(+), 154 deletions(-) delete mode 100644 web_src/css/codemirror/base.css create mode 100644 web_src/css/easymde.css delete mode 100644 web_src/css/editor/fileeditor.css diff --git a/web_src/css/codemirror/base.css b/web_src/css/codemirror/base.css deleted file mode 100644 index aedf7d85608..00000000000 --- a/web_src/css/codemirror/base.css +++ /dev/null @@ -1,49 +0,0 @@ -.ui .field:not(:last-child) .EasyMDEContainer .editor-statusbar { - margin-bottom: -1em; /* when there is a statusbar, the "margin-bottom: 1em" of the "field" is not needed, because the statusbar is likely a blank line */ -} - -.EasyMDEContainer .CodeMirror { - color: var(--color-input-text); - background-color: var(--color-input-background); - border-color: var(--color-secondary); - font: 14px var(--fonts-monospace); -} - -.EasyMDEContainer .CodeMirror.cm-s-default { - border-radius: var(--border-radius); - padding: 0 !important; -} - -.EasyMDEContainer .CodeMirror.CodeMirror-fullscreen.CodeMirror-focused { - border-right: 1px solid var(--color-primary) !important; -} - -.CodeMirror-cursor { - border-color: var(--color-caret) !important; -} - -.CodeMirror .cm-comment { - background: inherit !important; -} - -.CodeMirror .CodeMirror-code { - font: 14px var(--fonts-monospace); -} - -.CodeMirror-selected { - background: var(--color-primary-light-1) !important; - color: var(--color-white) !important; -} - -.CodeMirror-placeholder { - color: var(--color-placeholder-text) !important; - opacity: 1 !important; -} - -.CodeMirror-focused { - border-color: var(--color-primary) !important; -} - -.CodeMirror :focus { - outline: none; -} diff --git a/web_src/css/codemirror/dark.css b/web_src/css/codemirror/dark.css index 8a20d1c0043..0fcc13c076e 100644 --- a/web_src/css/codemirror/dark.css +++ b/web_src/css/codemirror/dark.css @@ -1,106 +1,106 @@ -.CodeMirror.cm-s-default .cm-property, -.CodeMirror.cm-s-paper .cm-property { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-property, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-property { color: #a0cc75; } -.CodeMirror.cm-s-default .cm-header, -.CodeMirror.cm-s-paper .cm-header { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-header, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-header { color: #9daccc; } -.CodeMirror.cm-s-default .cm-quote, -.CodeMirror.cm-s-paper .cm-quote { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-quote, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-quote { color: #009900; } -.CodeMirror.cm-s-default .cm-keyword, -.CodeMirror.cm-s-paper .cm-keyword { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-keyword, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-keyword { color: #cc8a61; } -.CodeMirror.cm-s-default .cm-atom, -.CodeMirror.cm-s-paper .cm-atom { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-atom, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-atom { color: #ef5e77; } -.CodeMirror.cm-s-default .cm-number, -.CodeMirror.cm-s-paper .cm-number { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-number, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-number { color: #ff5656; } -.CodeMirror.cm-s-default .cm-def, -.CodeMirror.cm-s-paper .cm-def { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-def, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-def { color: #e4e4e4; } -.CodeMirror.cm-s-default .cm-variable-2, -.CodeMirror.cm-s-paper .cm-variable-2 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-variable-2, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-variable-2 { color: #00bdbf; } -.CodeMirror.cm-s-default .cm-variable-3, -.CodeMirror.cm-s-paper .cm-variable-3 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-variable-3, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-variable-3 { color: #008855; } -.CodeMirror.cm-s-default .cm-comment, -.CodeMirror.cm-s-paper .cm-comment { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-comment, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-comment { color: #8e9ab3; } -.CodeMirror.cm-s-default .cm-string, -.CodeMirror.cm-s-paper .cm-string { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-string, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-string { color: #a77272; } -.CodeMirror.cm-s-default .cm-string-2, -.CodeMirror.cm-s-paper .cm-string-2 { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-string-2, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-string-2 { color: #ff5500; } -.CodeMirror.cm-s-default .cm-meta, -.CodeMirror.cm-s-paper .cm-meta, -.CodeMirror.cm-s-default .cm-qualifier, -.CodeMirror.cm-s-paper .cm-qualifier { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-meta, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-meta, +.EasyMDEContainer .CodeMirror.cm-s-default .cm-qualifier, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-qualifier { color: #ffb176; } -.CodeMirror.cm-s-default .cm-builtin, -.CodeMirror.cm-s-paper .cm-builtin { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-builtin, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-builtin { color: #b7c951; } -.CodeMirror.cm-s-default .cm-bracket, -.CodeMirror.cm-s-paper .cm-bracket { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-bracket, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-bracket { color: #999977; } -.CodeMirror.cm-s-default .cm-tag, -.CodeMirror.cm-s-paper .cm-tag { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-tag, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-tag { color: #f1d273; } -.CodeMirror.cm-s-default .cm-attribute, -.CodeMirror.cm-s-paper .cm-attribute { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-attribute, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-attribute { color: #bfcc70; } -.CodeMirror.cm-s-default .cm-hr, -.CodeMirror.cm-s-paper .cm-hr { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-hr, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-hr { color: #999999; } -.CodeMirror.cm-s-default .cm-url, -.CodeMirror.cm-s-paper .cm-url { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-url, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-url { color: #c5cfd0; } -.CodeMirror.cm-s-default .cm-link, -.CodeMirror.cm-s-paper .cm-link { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-link, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-link { color: #d8c792; } -.CodeMirror.cm-s-default .cm-error, -.CodeMirror.cm-s-paper .cm-error { +.EasyMDEContainer .CodeMirror.cm-s-default .cm-error, +.EasyMDEContainer .CodeMirror.cm-s-paper .cm-error { color: #dbdbeb; } diff --git a/web_src/css/easymde.css b/web_src/css/easymde.css new file mode 100644 index 00000000000..a3193bcfbed --- /dev/null +++ b/web_src/css/easymde.css @@ -0,0 +1,442 @@ +/* Inlined styles from easymde.min.css (includes EasyMDE and CodeMirror base) */ +.EasyMDEContainer { + display: block; +} + +/* CodeMirror base layout (from codemirror.css) */ +.EasyMDEContainer .CodeMirror { + position: relative; + overflow: hidden; + box-sizing: border-box; + height: auto; + border: 1px solid var(--color-input-border); + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + padding: 10px; + font: 14px var(--fonts-monospace); + z-index: 0; + overflow-wrap: break-word; + color: var(--color-input-text); + background-color: var(--color-input-background); + direction: ltr; +} + +.EasyMDEContainer .CodeMirror.cm-s-default { + border-radius: var(--border-radius); + padding: 0; +} + +.EasyMDEContainer .CodeMirror-lines { + padding: 4px 0; + cursor: text; + min-height: 1px; +} + +.EasyMDEContainer .CodeMirror pre.CodeMirror-line, +.EasyMDEContainer .CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + overflow-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + font-variant-ligatures: contextual; +} + +.EasyMDEContainer .CodeMirror-wrap pre.CodeMirror-line, +.EasyMDEContainer .CodeMirror-wrap pre.CodeMirror-line-like { + overflow-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.EasyMDEContainer .CodeMirror-scroll { + overflow: scroll !important; /* things will break if this is overridden */ + margin-bottom: -50px; + margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; + position: relative; + z-index: 0; + cursor: text; +} + +.EasyMDEContainer .CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; +} + +.EasyMDEContainer .CodeMirror-vscrollbar, +.EasyMDEContainer .CodeMirror-hscrollbar, +.EasyMDEContainer .CodeMirror-scrollbar-filler, +.EasyMDEContainer .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; + outline: none; +} + +.EasyMDEContainer .CodeMirror-vscrollbar { + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; +} + +.EasyMDEContainer .CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; +} + +.EasyMDEContainer .CodeMirror-scrollbar-filler { + right: 0; + bottom: 0; +} + +/* Cursor */ +.EasyMDEContainer .CodeMirror-cursor { + position: absolute; + pointer-events: none; + border-left: 1px solid var(--color-caret); + border-right: none; + width: 0; +} + +.EasyMDEContainer div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} + +.EasyMDEContainer div.CodeMirror-dragcursors { + visibility: visible; +} + +.EasyMDEContainer .CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +/* Selection */ +.EasyMDEContainer .CodeMirror-selected { + background: var(--color-primary-light-1); +} + +.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected { + background: var(--color-primary-light-1); +} + +.EasyMDEContainer .CodeMirror-line::selection, +.EasyMDEContainer .CodeMirror-line > span::selection, +.EasyMDEContainer .CodeMirror-line > span > span::selection { + background: var(--color-primary-light-1); +} + +/* Misc */ +.EasyMDEContainer .cm-tab { + display: inline-block; + text-decoration: inherit; +} + +.EasyMDEContainer .CodeMirror-rtl pre { + direction: rtl; +} + +.EasyMDEContainer .CodeMirror-code { + font: 14px var(--fonts-monospace); + outline: none; +} + +.EasyMDEContainer .CodeMirror-scroll, +.EasyMDEContainer .CodeMirror-sizer { + box-sizing: content-box; +} + +.EasyMDEContainer .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.EasyMDEContainer .CodeMirror-measure pre { + position: static; +} + +.EasyMDEContainer .CodeMirror-composing { + border-bottom: 2px solid; +} + +.EasyMDEContainer span.CodeMirror-selectedtext { + background: none; +} + +@media print { + .EasyMDEContainer .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* Default theme overrides */ +.EasyMDEContainer .cm-header, +.EasyMDEContainer .cm-strong { + font-weight: var(--font-weight-bold); +} + +.EasyMDEContainer .cm-em { + font-style: italic; +} + +.EasyMDEContainer .cm-link { + text-decoration: underline; +} + +.EasyMDEContainer .cm-strikethrough { + text-decoration: line-through; +} + +.EasyMDEContainer .cm-comment { + background: inherit; +} + +/* Placeholder */ +.EasyMDEContainer .CodeMirror-placeholder { + color: var(--color-placeholder-text); + opacity: 1; +} + +/* Focus */ +.EasyMDEContainer .CodeMirror-focused { + border-color: var(--color-primary); +} + +.EasyMDEContainer .CodeMirror :focus { + outline: none; +} + +/* Fullscreen */ +.EasyMDEContainer .CodeMirror-fullscreen { + background: var(--color-body); + position: fixed; + inset: 50px 0 0; + height: auto; + z-index: 8; + border-right: none; + border-bottom-right-radius: 0; +} + +.EasyMDEContainer .CodeMirror-fullscreen.CodeMirror-focused { + border-right: 1px solid var(--color-primary); +} + +/* Statusbar */ +.ui .field:not(:last-child) .EasyMDEContainer .editor-statusbar { + margin-bottom: -1em; /* when there is a statusbar, the "margin-bottom: 1em" of the "field" is not needed, because the statusbar is likely a blank line */ +} + +/* Toolbar */ +.EasyMDEContainer .editor-toolbar { + position: relative; + user-select: none; + padding: 9px 10px; + border-top: 1px solid var(--color-input-border); + border-left: 1px solid var(--color-input-border); + border-right: 1px solid var(--color-input-border); + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} + +.EasyMDEContainer .editor-toolbar.fullscreen { + width: 100%; + height: 50px; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + background: var(--color-body); + border: 0; + position: fixed; + top: 0; + left: 0; + opacity: 1; + z-index: 9; +} + +.EasyMDEContainer .editor-toolbar button { + background: transparent; + display: inline-block; + text-align: center; + text-decoration: none; + height: 30px; + margin: 0; + padding: 0 6px; + border: none; + border-radius: 3px; + cursor: pointer; + font-weight: var(--font-weight-bold); + min-width: 30px; + white-space: nowrap; + color: var(--color-text-light); +} + +.EasyMDEContainer .editor-toolbar button:not(:hover) { + background-color: transparent; +} + +.EasyMDEContainer .editor-toolbar button:hover { + background: var(--color-hover); +} + +.EasyMDEContainer .editor-toolbar button.active { + background: var(--color-active); +} + +.EasyMDEContainer .editor-toolbar i.separator { + display: inline-block; + width: 0; + border-left: none; + border-right: 1px solid var(--color-input-border); + color: transparent; + text-indent: -10px; + margin: 0 6px; +} + +.EasyMDEContainer .editor-toolbar button::after { + font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; + font-size: 65%; + vertical-align: text-bottom; + position: relative; + top: 2px; +} + +.EasyMDEContainer .editor-toolbar button.heading-1::after { + content: "1"; +} + +.EasyMDEContainer .editor-toolbar button.heading-2::after { + content: "2"; +} + +.EasyMDEContainer .editor-toolbar button.heading-3::after { + content: "3"; +} + +.EasyMDEContainer .editor-toolbar button.heading-bigger::after { + content: "\25B2"; +} + +.EasyMDEContainer .editor-toolbar button.heading-smaller::after { + content: "\25BC"; +} + +.EasyMDEContainer .editor-toolbar.disabled-for-preview button:not(.no-disable) { + opacity: 0.6; + pointer-events: none; +} + +/* hide preview button, we have the preview tab for this */ +.EasyMDEContainer .editor-toolbar:not(.fullscreen) .preview { + display: none; +} + +/* hide revert button in fullscreen, it breaks the page */ +.EasyMDEContainer .editor-toolbar.fullscreen .revert-to-textarea { + display: none; +} + +@media only screen and (max-width: 700px) { + .EasyMDEContainer .editor-toolbar i.no-mobile { + display: none; + } +} + +/* Statusbar */ +.EasyMDEContainer .editor-statusbar { + padding: 8px 10px; + font-size: 12px; + color: var(--color-text-light); + text-align: right; +} + +.EasyMDEContainer .editor-statusbar span { + display: inline-block; + min-width: 4em; + margin-left: 1em; +} + +.EasyMDEContainer .editor-statusbar .lines::before { + content: "lines: "; +} + +.EasyMDEContainer .editor-statusbar .words::before { + content: "words: "; +} + +.EasyMDEContainer .editor-statusbar .characters::before { + content: "characters: "; +} + +/* Preview */ +.EasyMDEContainer .editor-preview-full { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 7; + overflow: auto; + display: none; + box-sizing: border-box; +} + +.EasyMDEContainer .editor-preview-side { + position: fixed; + bottom: 0; + width: 50%; + top: 50px; + right: 0; + z-index: 9; + overflow: auto; + display: none; + box-sizing: border-box; + border: 1px solid var(--color-secondary); + overflow-wrap: break-word; +} + +.EasyMDEContainer .editor-preview-active-side { + display: block; +} + +.EasyMDEContainer .editor-preview-active { + display: block; +} + +.EasyMDEContainer .editor-preview { + padding: 10px; + background-color: var(--color-body); +} + +.EasyMDEContainer .editor-preview > p { + margin-top: 0; +} + +.EasyMDEContainer .editor-preview pre { + background: var(--color-markup-code-block); + margin-bottom: 10px; +} + +.EasyMDEContainer .editor-preview table td, +.EasyMDEContainer .editor-preview table th { + border: 1px solid var(--color-secondary); + padding: 5px; +} diff --git a/web_src/css/editor/fileeditor.css b/web_src/css/editor/fileeditor.css deleted file mode 100644 index 12ae97a1094..00000000000 --- a/web_src/css/editor/fileeditor.css +++ /dev/null @@ -1,56 +0,0 @@ -.editor-toolbar { - border-color: var(--color-secondary); -} - -.editor-toolbar.fullscreen { - background: var(--color-body); -} - -.editor-toolbar button { - border: none !important; - color: var(--color-text-light); -} - -.editor-toolbar button:not(:hover) { - background-color: transparent !important; -} - -.editor-toolbar i.separator { - border-left: none; - border-right-color: var(--color-secondary); -} - -.editor-toolbar button:hover { - background: var(--color-hover); -} - -.editor-toolbar button.active { - background: var(--color-active); -} - -/* hide preview button, we have the preview tab for this */ -.editor-toolbar:not(.fullscreen) .preview { - display: none; -} - -/* hide revert button in fullscreen, it breaks the page */ -.editor-toolbar.fullscreen .revert-to-textarea { - display: none; -} - -.editor-preview { - background-color: var(--color-body); -} - -.editor-preview-side { - border-color: var(--color-secondary); -} - -.editor-statusbar { - color: var(--color-text-light); -} - -.editor-loading { - padding: 1rem; - text-align: center; -} diff --git a/web_src/css/features/codeeditor.css b/web_src/css/features/codeeditor.css index 8df3429b09b..33a9191f406 100644 --- a/web_src/css/features/codeeditor.css +++ b/web_src/css/features/codeeditor.css @@ -1,3 +1,8 @@ +.editor-loading { + padding: 1rem; + text-align: center; +} + .monaco-editor-container, .editor-loading.is-loading { width: 100%; diff --git a/web_src/css/index.css b/web_src/css/index.css index c02651d5202..699ba221ca5 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -52,7 +52,6 @@ @import "./markup/asciicast.css"; @import "./chroma/base.css"; -@import "./codemirror/base.css"; @import "./font_i18n.css"; @import "./base.css"; @import "./home.css"; @@ -74,7 +73,6 @@ @import "./repo/commit-sign.css"; @import "./repo/packages.css"; -@import "./editor/fileeditor.css"; @import "./editor/combomarkdowneditor.css"; @import "./org.css"; diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index fdc8a1d601e..42104947df8 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -318,8 +318,10 @@ export class ComboMarkdownEditor { async switchToEasyMDE() { if (this.easyMDE) return; - // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. - const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); + const [{default: EasyMDE}] = await Promise.all([ + import(/* webpackChunkName: "easymde" */'easymde'), + import(/* webpackChunkName: "easymde" */'../../../css/easymde.css'), + ]); const easyMDEOpt: EasyMDE.Options = { autoDownloadFontAwesome: false, element: this.textarea, diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index 187876df445..fb445b8df42 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,5 +1,4 @@ import '../fomantic/build/fomantic.js'; -import '../../node_modules/easymde/dist/easymde.min.css'; // TODO: lazy load in "switchToEasyMDE" import {initHtmx} from './htmx.ts'; import {initDashboardRepoList} from './features/dashboard.ts'; From d0f92cb0a133c325323121ad391fbf043e3a6edb Mon Sep 17 00:00:00 2001 From: danigm Date: Thu, 26 Feb 2026 12:56:02 +0100 Subject: [PATCH 02/50] Add created_by filter to SearchIssues (#36670) This patch adds the created_by filter to the SearchIssues method. tea cli has an option to filter by author when listing issues, but it's not working. The tea command line creates this request for the API when using the author filter: ``` $ tea issue list -l local --kind pull -A danigm -vvv http://localhost:3000/api/v1/repos/issues/search?created_by=danigm&labels=&limit=30&milestones=&page=1&state=open&type=pulls ``` This patch fixes the API to allow this kind of queries from go-sdk and tea cli. --------- Co-authored-by: wxiaoguang Co-authored-by: silverwind --- routers/api/v1/repo/issue.go | 12 ++++++++++++ templates/swagger/v1_json.tmpl | 6 ++++++ tests/integration/api_issue_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 41076fd99c8..22324e19233 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -157,6 +157,10 @@ func SearchIssues(ctx *context.APIContext) { // in: query // description: Filter by repository owner // type: string + // - name: created_by + // in: query + // description: Only show items which were created by the given user + // type: string // - name: team // in: query // description: Filter by team (requires organization owner parameter) @@ -257,6 +261,14 @@ func SearchIssues(ctx *context.APIContext) { searchOpt.UpdatedBeforeUnix = optional.Some(before) } + createdByID := getUserIDForFilter(ctx, "created_by") + if ctx.Written() { + return + } + if createdByID > 0 { + searchOpt.PosterID = strconv.FormatInt(createdByID, 10) + } + if ctx.IsSigned { ctxUserID := ctx.Doer.ID if ctx.FormBool("created") { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index a1ecc7fb4fe..7b86cc3d45b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -4300,6 +4300,12 @@ "name": "owner", "in": "query" }, + { + "type": "string", + "description": "Only show items which were created by the given user", + "name": "created_by", + "in": "query" + }, { "type": "string", "description": "Filter by team (requires organization owner parameter)", diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index 56bed7db0d3..8d85543dc8a 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -361,6 +361,34 @@ func TestAPISearchIssues(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 2) + + query = url.Values{"created": {"1"}} // issues created by the auth user + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 5) + + query = url.Values{"created": {"1"}, "type": {"pulls"}} // prs created by the auth user + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 3) + + query = url.Values{"created_by": {"user2"}} // issues created by the user2 + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 9) + + query = url.Values{"created_by": {"user2"}, "type": {"pulls"}} // prs created by user2 + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 3) } func TestAPISearchIssuesWithLabels(t *testing.T) { From 26d83c932a8cc6f6f984a76d6b57945f99664cb1 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Thu, 26 Feb 2026 16:16:11 +0100 Subject: [PATCH 03/50] Instance-wide (global) info banner and maintenance mode (#36571) The banner allows site operators to communicate important announcements (e.g., maintenance windows, policy updates, service notices) directly within the UI. The maintenance mode only allows admin to access the web UI. * Fix #2345 * Fix #9618 --------- Co-authored-by: wxiaoguang --- modules/markup/sanitizer_default.go | 2 +- modules/setting/config.go | 26 ++- modules/setting/config/value.go | 163 +++++++++---- modules/setting/config_option_instance.go | 58 +++++ modules/web/middleware/cookie.go | 6 +- options/locale/locale_en-US.json | 8 + routers/common/errpage.go | 5 +- routers/common/maintenancemode.go | 43 ++++ routers/init.go | 1 + routers/private/internal.go | 2 + routers/web/admin/config.go | 78 ++----- routers/web/auth/auth.go | 5 + routers/web/misc/misc.go | 9 + routers/web/misc/webtheme.go | 2 +- routers/web/repo/view_home.go | 3 - routers/web/web.go | 2 +- services/context/context.go | 10 +- services/context/context_template.go | 25 +- .../config_settings/config_settings.tmpl | 8 +- templates/admin/config_settings/instance.tmpl | 63 ++++++ .../admin/config_settings/repository.tmpl | 15 +- templates/admin/layout_head.tmpl | 2 +- templates/base/head_banner.tmpl | 11 + templates/base/head_navbar.tmpl | 1 + templates/shared/combomarkdowneditor.tmpl | 8 +- tests/integration/admin_config_test.go | 46 ++++ tests/integration/config_instance_test.go | 126 +++++++++++ web_src/css/admin.css | 8 + web_src/css/modules/container.css | 18 ++ web_src/js/features/admin/config.test.ts | 41 ++++ web_src/js/features/admin/config.ts | 214 ++++++++++++++++-- web_src/js/features/common-fetch-action.ts | 13 +- .../js/features/comp/ComboMarkdownEditor.ts | 4 +- web_src/js/features/repo-editor.ts | 2 +- 34 files changed, 870 insertions(+), 158 deletions(-) create mode 100644 modules/setting/config_option_instance.go create mode 100644 routers/common/maintenancemode.go create mode 100644 templates/admin/config_settings/instance.tmpl create mode 100644 templates/base/head_banner.tmpl create mode 100644 tests/integration/config_instance_test.go create mode 100644 web_src/js/features/admin/config.test.ts diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 7fdf66c4bce..77ba8bf4f4c 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -81,7 +81,7 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { "data-markdown-generated-content", "data-attr-class", } generalSafeElements := []string{ - "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt", + "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "center", "i", "strong", "em", "a", "pre", "code", "img", "tt", "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label", "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary", "details", "caption", "figure", "figcaption", diff --git a/modules/setting/config.go b/modules/setting/config.go index fb99325a956..bde8e4ac2ab 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -12,8 +12,8 @@ import ( ) type PictureStruct struct { - DisableGravatar *config.Value[bool] - EnableFederatedAvatar *config.Value[bool] + DisableGravatar *config.Option[bool] + EnableFederatedAvatar *config.Option[bool] } type OpenWithEditorApp struct { @@ -23,6 +23,9 @@ type OpenWithEditorApp struct { type OpenWithEditorAppsType []OpenWithEditorApp +// ToTextareaString is only used in templates, for help prompt only +// TODO: OPEN-WITH-EDITOR-APP-JSON: Because there is no "rich UI", a plain text editor is used to manage the list of apps +// Maybe we can use some better formats like Yaml in the future, then a simple textarea can manage the config clearly func (t OpenWithEditorAppsType) ToTextareaString() string { var ret strings.Builder for _, app := range t { @@ -31,7 +34,7 @@ func (t OpenWithEditorAppsType) ToTextareaString() string { return ret.String() } -func DefaultOpenWithEditorApps() OpenWithEditorAppsType { +func openWithEditorAppsDefaultValue() OpenWithEditorAppsType { return OpenWithEditorAppsType{ { DisplayName: "VS Code", @@ -49,13 +52,14 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType { } type RepositoryStruct struct { - OpenWithEditorApps *config.Value[OpenWithEditorAppsType] - GitGuideRemoteName *config.Value[string] + OpenWithEditorApps *config.Option[OpenWithEditorAppsType] + GitGuideRemoteName *config.Option[string] } type ConfigStruct struct { Picture *PictureStruct Repository *RepositoryStruct + Instance *InstanceStruct } var ( @@ -67,12 +71,16 @@ func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ Picture: &PictureStruct{ - DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), - EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), + DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), + EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), }, Repository: &RepositoryStruct{ - OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"), - GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"), + OpenWithEditorApps: config.NewOption[OpenWithEditorAppsType]("repository.open-with.editor-apps").WithEmptyAsDefault().WithDefaultFunc(openWithEditorAppsDefaultValue), + GitGuideRemoteName: config.NewOption[string]("repository.git-guide-remote-name").WithEmptyAsDefault().WithDefaultSimple("origin"), + }, + Instance: &InstanceStruct{ + WebBanner: config.NewOption[WebBannerType]("instance.web_banner"), + MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"), }, } } diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index 301c60f5e82..bd91add97a9 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -5,6 +5,7 @@ package config import ( "context" + "reflect" "sync" "code.gitea.io/gitea/modules/json" @@ -16,18 +17,31 @@ type CfgSecKey struct { Sec, Key string } -type Value[T any] struct { +// OptionInterface is used to overcome Golang's generic interface limitation +type OptionInterface interface { + GetDefaultValue() any +} + +type Option[T any] struct { mu sync.RWMutex cfgSecKey CfgSecKey dynKey string - def, value T + value T + defSimple T + defFunc func() T + emptyAsDef bool + has bool revision int } -func (value *Value[T]) parse(key, valStr string) (v T) { - v = value.def +func (opt *Option[T]) GetDefaultValue() any { + return opt.DefaultValue() +} + +func (opt *Option[T]) parse(key, valStr string) (v T) { + v = opt.DefaultValue() if valStr != "" { if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil { log.Error("Unable to unmarshal json config for key %q, err: %v", key, err) @@ -36,7 +50,35 @@ func (value *Value[T]) parse(key, valStr string) (v T) { return v } -func (value *Value[T]) Value(ctx context.Context) (v T) { +func (opt *Option[T]) HasValue(ctx context.Context) bool { + _, _, has := opt.ValueRevision(ctx) + return has +} + +func (opt *Option[T]) Value(ctx context.Context) (v T) { + v, _, _ = opt.ValueRevision(ctx) + return v +} + +func isZeroOrEmpty(v any) bool { + if v == nil { + return true // interface itself is nil + } + r := reflect.ValueOf(v) + if r.IsZero() { + return true + } + + if r.Kind() == reflect.Slice || r.Kind() == reflect.Map { + if r.IsNil() { + return true + } + return r.Len() == 0 + } + return false +} + +func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) { dg := GetDynGetter() if dg == nil { // this is an edge case: the database is not initialized but the system setting is going to be used @@ -44,55 +86,96 @@ func (value *Value[T]) Value(ctx context.Context) (v T) { panic("no config dyn value getter") } - rev := dg.GetRevision(ctx) + rev = dg.GetRevision(ctx) // if the revision in the database doesn't change, use the last value - value.mu.RLock() - if rev == value.revision { - v = value.value - value.mu.RUnlock() - return v + opt.mu.RLock() + if rev == opt.revision { + v = opt.value + has = opt.has + opt.mu.RUnlock() + return v, rev, has } - value.mu.RUnlock() + opt.mu.RUnlock() // try to parse the config and cache it var valStr *string - if dynVal, has := dg.GetValue(ctx, value.dynKey); has { + if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue { valStr = &dynVal - } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has { + } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); has { valStr = &cfgVal } if valStr == nil { - v = value.def + v = opt.DefaultValue() + has = false } else { - v = value.parse(value.dynKey, *valStr) + v = opt.parse(opt.dynKey, *valStr) + if opt.emptyAsDef && isZeroOrEmpty(v) { + v = opt.DefaultValue() + } else { + has = true + } } - value.mu.Lock() - value.value = v - value.revision = rev - value.mu.Unlock() + opt.mu.Lock() + opt.value = v + opt.revision = rev + opt.has = has + opt.mu.Unlock() + return v, rev, has +} + +func (opt *Option[T]) DynKey() string { + return opt.dynKey +} + +// WithDefaultFunc sets the default value with a function +// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice +func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] { + opt.defFunc = f + return opt +} + +func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] { + v := any(def) + switch v.(type) { + case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + default: + // TODO: use reflect to support convertable basic types like `type State string` + r := reflect.ValueOf(v) + if r.Kind() != reflect.Struct { + panic("invalid type for default value, use WithDefaultFunc instead") + } + } + opt.defSimple = def + return opt +} + +func (opt *Option[T]) WithEmptyAsDefault() *Option[T] { + opt.emptyAsDef = true + return opt +} + +func (opt *Option[T]) DefaultValue() T { + if opt.defFunc != nil { + return opt.defFunc() + } + return opt.defSimple +} + +func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] { + opt.cfgSecKey = cfgSecKey + return opt +} + +var allConfigOptions = map[string]OptionInterface{} + +func NewOption[T any](dynKey string) *Option[T] { + v := &Option[T]{dynKey: dynKey} + allConfigOptions[dynKey] = v return v } -func (value *Value[T]) DynKey() string { - return value.dynKey -} - -func (value *Value[T]) WithDefault(def T) *Value[T] { - value.def = def - return value -} - -func (value *Value[T]) DefaultValue() T { - return value.def -} - -func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] { - value.cfgSecKey = cfgSecKey - return value -} - -func ValueJSON[T any](dynKey string) *Value[T] { - return &Value[T]{dynKey: dynKey} +func GetConfigOption(dynKey string) OptionInterface { + return allConfigOptions[dynKey] } diff --git a/modules/setting/config_option_instance.go b/modules/setting/config_option_instance.go new file mode 100644 index 00000000000..6d97055a75e --- /dev/null +++ b/modules/setting/config_option_instance.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "time" + + "code.gitea.io/gitea/modules/setting/config" +) + +// WebBannerType fields are directly used in templates, +// do remember to update the template if you change the fields +type WebBannerType struct { + DisplayEnabled bool + ContentMessage string + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (b WebBannerType) ShouldDisplay() bool { + if !b.DisplayEnabled || b.ContentMessage == "" { + return false + } + now := time.Now().Unix() + if b.StartTimeUnix > 0 && now < b.StartTimeUnix { + return false + } + if b.EndTimeUnix > 0 && now > b.EndTimeUnix { + return false + } + return true +} + +type MaintenanceModeType struct { + AdminWebAccessOnly bool + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (m MaintenanceModeType) IsActive() bool { + if !m.AdminWebAccessOnly { + return false + } + now := time.Now().Unix() + if m.StartTimeUnix > 0 && now < m.StartTimeUnix { + return false + } + if m.EndTimeUnix > 0 && now > m.EndTimeUnix { + return false + } + return true +} + +type InstanceStruct struct { + WebBanner *config.Option[WebBannerType] + MaintenanceMode *config.Option[MaintenanceModeType] +} diff --git a/modules/web/middleware/cookie.go b/modules/web/middleware/cookie.go index f98aceba101..336c276fe8f 100644 --- a/modules/web/middleware/cookie.go +++ b/modules/web/middleware/cookie.go @@ -14,7 +14,11 @@ import ( "code.gitea.io/gitea/modules/util" ) -const cookieRedirectTo = "redirect_to" +const ( + CookieWebBannerDismissed = "gitea_disbnr" + CookieTheme = "gitea_theme" + cookieRedirectTo = "redirect_to" +) func GetRedirectToCookie(req *http.Request) string { return GetSiteCookie(req, cookieRedirectTo) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 97e2ebe0d1d..bcd28f2deba 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -84,6 +84,7 @@ "save": "Save", "add": "Add", "add_all": "Add All", + "dismiss": "Dismiss", "remove": "Remove", "remove_all": "Remove All", "remove_label_str": "Remove item \"%s\"", @@ -3278,6 +3279,13 @@ "admin.config.cache_test_failed": "Failed to probe the cache: %v.", "admin.config.cache_test_slow": "Cache test successful, but response is slow: %s.", "admin.config.cache_test_succeeded": "Cache test successful, got a response in %s.", + "admin.config.common.start_time": "Start time", + "admin.config.common.end_time": "End time", + "admin.config.common.skip_time_check": "Leave time empty (clear the field) to skip time check", + "admin.config.instance_maintenance": "Instance Maintenance", + "admin.config.instance_maintenance_mode.admin_web_access_only": "Only allow admin to access the web UI", + "admin.config.instance_web_banner.enabled": "Show banner", + "admin.config.instance_web_banner.message_placeholder": "Banner message (supports markdown)", "admin.config.session_config": "Session Configuration", "admin.config.session_provider": "Session Provider", "admin.config.provider_config": "Provider Config", diff --git a/routers/common/errpage.go b/routers/common/errpage.go index b14ab8bcf80..2406cf443fa 100644 --- a/routers/common/errpage.go +++ b/routers/common/errpage.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web/middleware" @@ -36,9 +37,7 @@ func renderServerErrorPage(w http.ResponseWriter, req *http.Request, respCode in w.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions) } - tmplCtx := context.NewTemplateContext(req.Context(), req) - tmplCtx["Locale"] = middleware.Locale(w, req) - + tmplCtx := context.NewTemplateContextForWeb(reqctx.FromContext(req.Context()), req, middleware.Locale(w, req)) w.WriteHeader(respCode) outBuf := &bytes.Buffer{} diff --git a/routers/common/maintenancemode.go b/routers/common/maintenancemode.go new file mode 100644 index 00000000000..b5827ac94f4 --- /dev/null +++ b/routers/common/maintenancemode.go @@ -0,0 +1,43 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package common + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +func isMaintenanceModeAllowedRequest(req *http.Request) bool { + if strings.HasPrefix(req.URL.Path, "/-/") { + // URLs like "/-/admin", "/-/fetch-redirect" and "/-/markup" are still accessible in maintenance mode + return true + } + if strings.HasPrefix(req.URL.Path, "/api/internal/") { + // internal APIs should be allowed + return true + } + if strings.HasPrefix(req.URL.Path, "/user/") { + // URLs like "/user/signin" and "/user/signup" are still accessible in maintenance mode + return true + } + if strings.HasPrefix(req.URL.Path, "/assets/") { + return true + } + return false +} + +func MaintenanceModeHandler() func(h http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + maintenanceMode := setting.Config().Instance.MaintenanceMode.Value(req.Context()) + if maintenanceMode.IsActive() && !isMaintenanceModeAllowedRequest(req) { + renderServiceUnavailable(resp, req) + return + } + next.ServeHTTP(resp, req) + }) + } +} diff --git a/routers/init.go b/routers/init.go index 82a5378263d..8874236a607 100644 --- a/routers/init.go +++ b/routers/init.go @@ -181,6 +181,7 @@ func InitWebInstalled(ctx context.Context) { func NormalRoutes() *web.Router { r := web.NewRouter() r.Use(common.ProtocolMiddlewares()...) + r.Use(common.MaintenanceModeHandler()) r.Mount("/", web_routers.Routes()) r.Mount("/api/v1", apiv1.Routes()) diff --git a/routers/private/internal.go b/routers/private/internal.go index 55a11aa3dda..2d5436468b6 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/common" + "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/services/context" "gitea.com/go-chi/binding" @@ -59,6 +60,7 @@ func Routes() *web.Router { // Since internal API will be sent only from Gitea sub commands and it's under control (checked by InternalToken), we can trust the headers. r.Use(chi_middleware.RealIP) + r.Get("/dummy", misc.DummyOK) r.Post("/ssh/authorized_keys", AuthorizedPublicKeyByContent) r.Post("/ssh/{id}/update/{repoid}", UpdatePublicKeyInRepo) r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 774b31ab984..79e969fd5e4 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -5,9 +5,9 @@ package admin import ( + "errors" "net/http" "net/url" - "strconv" "strings" system_model "code.gitea.io/gitea/models/system" @@ -145,7 +145,6 @@ func Config(ctx *context.Context) { ctx.Data["Service"] = setting.Service ctx.Data["DbCfg"] = setting.Database ctx.Data["Webhook"] = setting.Webhook - ctx.Data["MailerEnabled"] = false if setting.MailService != nil { ctx.Data["MailerEnabled"] = true @@ -191,52 +190,27 @@ func ConfigSettings(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.config_settings") ctx.Data["PageIsAdminConfig"] = true ctx.Data["PageIsAdminConfigSettings"] = true - ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString() ctx.HTML(http.StatusOK, tplConfigSettings) } +func validateConfigKeyValue(dynKey, input string) error { + opt := config.GetConfigOption(dynKey) + if opt == nil { + return util.NewInvalidArgumentErrorf("unknown config key: %s", dynKey) + } + + const limit = 64 * 1024 + if len(input) > limit { + return util.NewInvalidArgumentErrorf("value length exceeds limit of %d", limit) + } + + if !json.Valid([]byte(input)) { + return util.NewInvalidArgumentErrorf("invalid json value for key: %s", dynKey) + } + return nil +} + func ChangeConfig(ctx *context.Context) { - cfg := setting.Config() - - marshalBool := func(v string) ([]byte, error) { - b, _ := strconv.ParseBool(v) - return json.Marshal(b) - } - - marshalString := func(emptyDefault string) func(v string) ([]byte, error) { - return func(v string) ([]byte, error) { - return json.Marshal(util.IfZero(v, emptyDefault)) - } - } - - marshalOpenWithApps := func(value string) ([]byte, error) { - // TODO: move the block alongside OpenWithEditorAppsType.ToTextareaString - lines := strings.Split(value, "\n") - var openWithEditorApps setting.OpenWithEditorAppsType - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - displayName, openURL, ok := strings.Cut(line, "=") - displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL) - if !ok || displayName == "" || openURL == "" { - continue - } - openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{ - DisplayName: strings.TrimSpace(displayName), - OpenURL: strings.TrimSpace(openURL), - }) - } - return json.Marshal(openWithEditorApps) - } - marshallers := map[string]func(string) ([]byte, error){ - cfg.Picture.DisableGravatar.DynKey(): marshalBool, - cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool, - cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps, - cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()), - } - _ = ctx.Req.ParseForm() configKeys := ctx.Req.Form["key"] configValues := ctx.Req.Form["value"] @@ -249,18 +223,16 @@ loop: } value := configValues[i] - marshaller, hasMarshaller := marshallers[key] - if !hasMarshaller { - ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) - break loop - } - - marshaledValue, err := marshaller(value) + err := validateConfigKeyValue(key, value) if err != nil { - ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) + if errors.Is(err, util.ErrInvalidArgument) { + ctx.JSONError(err.Error()) + } else { + ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key)) + } break loop } - configSettings[key] = string(marshaledValue) + configSettings[key] = value } if ctx.Written() { return diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index bc0939d92a1..9529525a273 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -162,6 +162,11 @@ func consumeAuthRedirectLink(ctx *context.Context) string { } func redirectAfterAuth(ctx *context.Context) { + if setting.Config().Instance.MaintenanceMode.Value(ctx).IsActive() { + // in maintenance mode, redirect to admin dashboard, it is the only accessible page + ctx.Redirect(setting.AppSubURL + "/-/admin") + return + } ctx.RedirectToCurrentSite(consumeAuthRedirectLink(ctx)) } diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index 59b97c17175..3d2f624263b 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -6,12 +6,15 @@ package misc import ( "net/http" "path" + "strconv" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/context" ) func SSHInfo(rw http.ResponseWriter, req *http.Request) { @@ -47,3 +50,9 @@ func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently) } } + +func WebBannerDismiss(ctx *context.Context) { + _, rev, _ := setting.Config().Instance.WebBanner.ValueRevision(ctx) + middleware.SetSiteCookie(ctx.Resp, middleware.CookieWebBannerDismissed, strconv.Itoa(rev), 48*3600) + ctx.JSONOK() +} diff --git a/routers/web/misc/webtheme.go b/routers/web/misc/webtheme.go index 076bdf8fda2..76ddf4b567a 100644 --- a/routers/web/misc/webtheme.go +++ b/routers/web/misc/webtheme.go @@ -37,6 +37,6 @@ func WebThemeApply(ctx *context.Context) { opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)} _ = user_service.UpdateUser(ctx, ctx.Doer, opts) } else { - middleware.SetSiteCookie(ctx.Resp, "gitea_theme", themeName, 0) + middleware.SetSiteCookie(ctx.Resp, middleware.CookieTheme, themeName, 0) } } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 00d30bedef5..d1a969cf2d7 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -69,9 +69,6 @@ func prepareHomeSidebarRepoTopics(ctx *context.Context) { func prepareOpenWithEditorApps(ctx *context.Context) { var tmplApps []map[string]any apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) - if len(apps) == 0 { - apps = setting.DefaultOpenWithEditorApps() - } for _, app := range apps { schema, _, _ := strings.Cut(app.OpenURL, ":") diff --git a/routers/web/web.go b/routers/web/web.go index b1b31a7ec9d..ce037afe1b3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -480,7 +480,7 @@ func registerWebRoutes(m *web.Router) { }, optionsCorsHandler()) m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup) - + m.Post("/-/web-banner/dismiss", misc.WebBannerDismiss) m.Get("/-/web-theme/list", misc.WebThemeList) m.Post("/-/web-theme/apply", optSignIn, misc.WebThemeApply) diff --git a/services/context/context.go b/services/context/context.go index ccd0057f597..97b9890f436 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -100,12 +100,12 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) { return ctx } -func NewTemplateContextForWeb(ctx *Context) TemplateContext { - tmplCtx := NewTemplateContext(ctx, ctx.Req) - tmplCtx["Locale"] = ctx.Base.Locale +func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, locale translation.Locale) TemplateContext { + tmplCtx := NewTemplateContext(ctx, req) + tmplCtx["Locale"] = locale tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx) tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx) - tmplCtx["RootData"] = ctx.Data + tmplCtx["RootData"] = ctx.GetData() tmplCtx["Consts"] = map[string]any{ "RepoUnitTypeCode": unit.TypeCode, "RepoUnitTypeIssues": unit.TypeIssues, @@ -132,7 +132,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Repo: &Repository{}, Org: &Organization{}, } - ctx.TemplateContext = NewTemplateContextForWeb(ctx) + ctx.TemplateContext = NewTemplateContextForWeb(ctx, ctx.Base.Req, ctx.Base.Locale) ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}} ctx.SetContextValue(WebContextKey, ctx) return ctx diff --git a/services/context/context_template.go b/services/context/context_template.go index c1045136ee9..52c74611878 100644 --- a/services/context/context_template.go +++ b/services/context/context_template.go @@ -6,8 +6,11 @@ package context import ( "context" "net/http" + "strconv" "time" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/webtheme" ) @@ -17,6 +20,10 @@ func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext return TemplateContext{"_ctx": ctx, "_req": req} } +func (c TemplateContext) req() *http.Request { + return c["_req"].(*http.Request) +} + func (c TemplateContext) parentContext() context.Context { return c["_ctx"].(context.Context) } @@ -38,7 +45,6 @@ func (c TemplateContext) Value(key any) any { } func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo { - req := c["_req"].(*http.Request) var themeName string if webCtx := GetWebContext(c); webCtx != nil { if webCtx.Doer != nil { @@ -46,9 +52,20 @@ func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo { } } if themeName == "" { - if cookieTheme, _ := req.Cookie("gitea_theme"); cookieTheme != nil { - themeName = cookieTheme.Value - } + themeName = middleware.GetSiteCookie(c.req(), middleware.CookieTheme) } return webtheme.GuaranteeGetThemeMetaInfo(themeName) } + +func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType { + // Using revision as a simple approach to determine if the banner has been changed after the user dismissed it. + // There could be some false-positives because revision can be changed even if the banner isn't. + // While it should be still good enough (no admin would keep changing the settings) and doesn't really harm end users (just a few more times to see the banner) + // So it doesn't need to make it more complicated by allocating unique IDs or using hashes. + dismissedBannerRevision, _ := strconv.Atoi(middleware.GetSiteCookie(c.req(), middleware.CookieWebBannerDismissed)) + banner, revision, _ := setting.Config().Instance.WebBanner.ValueRevision(c) + if banner.ShouldDisplay() && dismissedBannerRevision != revision { + return &banner + } + return nil +} diff --git a/templates/admin/config_settings/config_settings.tmpl b/templates/admin/config_settings/config_settings.tmpl index 1ef764a58ba..6d1db4f89fd 100644 --- a/templates/admin/config_settings/config_settings.tmpl +++ b/templates/admin/config_settings/config_settings.tmpl @@ -1,7 +1,7 @@ -{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}} +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config" "dataGlobalInit" "initAdminConfigSettings")}} -{{template "admin/config_settings/avatars" .}} - -{{template "admin/config_settings/repository" .}} + {{template "admin/config_settings/avatars" .}} + {{template "admin/config_settings/repository" .}} + {{template "admin/config_settings/instance" .}} {{template "admin/layout_footer" .}} diff --git a/templates/admin/config_settings/instance.tmpl b/templates/admin/config_settings/instance.tmpl new file mode 100644 index 00000000000..da28fffddb4 --- /dev/null +++ b/templates/admin/config_settings/instance.tmpl @@ -0,0 +1,63 @@ +

{{ctx.Locale.Tr "admin.config.instance_maintenance"}}

+
+
+ {{$cfgOpt := $.SystemConfig.Instance.MaintenanceMode}} + {{$cfgKey := $cfgOpt.DynKey}} + {{$maintenanceMode := $cfgOpt.Value ctx}} + +
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
{{ctx.Locale.Tr "admin.config.common.skip_time_check"}}
+
+ +
+ + {{$cfgOpt = $.SystemConfig.Instance.WebBanner}} + {{$cfgKey = $cfgOpt.DynKey}} + {{$banner := $cfgOpt.Value ctx}} + +
+
+ + +
+ {{template "shared/combomarkdowneditor" (dict + "ContainerClasses" "web-banner-content-editor" + "TextareaName" (print $cfgKey ".ContentMessage") + "TextareaContent" $banner.ContentMessage + "TextareaPlaceholder" (ctx.Locale.Tr "admin.config.instance_web_banner.message_placeholder") + )}} +
+
+
+
+ + +
+
+ + +
+
+
{{ctx.Locale.Tr "admin.config.common.skip_time_check"}}
+
+
+ +
+
+
diff --git a/templates/admin/config_settings/repository.tmpl b/templates/admin/config_settings/repository.tmpl index 9a377078356..2d5845ba4ed 100644 --- a/templates/admin/config_settings/repository.tmpl +++ b/templates/admin/config_settings/repository.tmpl @@ -2,24 +2,23 @@ {{ctx.Locale.Tr "repository"}}
-
+ + {{$cfg := .SystemConfig.Repository.OpenWithEditorApps}}
{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}} -
{{.DefaultOpenWithEditorAppsString}}
+
{{$cfg.DefaultValue.ToTextareaString}}
- {{$cfg := .SystemConfig.Repository.OpenWithEditorApps}} - - + {{/* TODO: OPEN-WITH-EDITOR-APP-JSON: use a simple textarea */}} +
+ {{$cfg = .SystemConfig.Repository.GitGuideRemoteName}}
- {{$cfg = .SystemConfig.Repository.GitGuideRemoteName}} - - +
diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 7cc6624d504..397516da5da 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .ctxData}} -
+
{{template "admin/navbar" .ctxData}}
diff --git a/templates/base/head_banner.tmpl b/templates/base/head_banner.tmpl new file mode 100644 index 00000000000..d237161622a --- /dev/null +++ b/templates/base/head_banner.tmpl @@ -0,0 +1,11 @@ +{{$banner := ctx.CurrentWebBanner}} +{{if $banner}} +
+
+ {{ctx.RenderUtils.MarkdownToHtml $banner.ContentMessage}} +
+ +
+{{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index cda1f377b40..28fcee023fc 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -176,3 +176,4 @@
{{end}} +{{template "base/head_banner"}} diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 1c48ebbb95a..3c0759f9b2e 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -4,7 +4,7 @@ * ContainerClasses: additional classes for the container element * MarkdownPreviewInRepo: the repo to preview markdown * MarkdownPreviewContext: preview context (the related url path when rendering) for the preview tab, eg: repo link or user home link -* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default +* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default, can be disabled by "none" * TextareaName: name attribute for the textarea * TextareaContent: content for the textarea * TextareaMaxLength: maxlength attribute for the textarea @@ -29,10 +29,12 @@ data-preview-url="{{$previewUrl}}" data-preview-context="{{$previewContext}}" > + {{if ne $previewMode "none"}} + {{end}}
@@ -87,9 +89,9 @@
- + x - +
diff --git a/tests/integration/admin_config_test.go b/tests/integration/admin_config_test.go index eec7e75fd91..5f882e8a550 100644 --- a/tests/integration/admin_config_test.go +++ b/tests/integration/admin_config_test.go @@ -7,10 +7,14 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAdminConfig(t *testing.T) { @@ -20,4 +24,46 @@ func TestAdminConfig(t *testing.T) { req := NewRequest(t, "GET", "/-/admin/config") resp := session.MakeRequest(t, req, http.StatusOK) assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) + + t.Run("OpenEditorWithApps", func(t *testing.T) { + cfg := setting.Config().Repository.OpenWithEditorApps + editorApps := cfg.Value(t.Context()) + assert.Len(t, editorApps, 3) + assert.False(t, cfg.HasValue(t.Context())) + + require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[]"})) + config.GetDynGetter().InvalidateCache() + + editorApps = cfg.Value(t.Context()) + assert.Len(t, editorApps, 3) + assert.False(t, cfg.HasValue(t.Context())) + + require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[{}]"})) + config.GetDynGetter().InvalidateCache() + + editorApps = cfg.Value(t.Context()) + assert.Len(t, editorApps, 1) + assert.True(t, cfg.HasValue(t.Context())) + }) + + t.Run("InstanceWebBanner", func(t *testing.T) { + banner, rev1, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context()) + assert.False(t, has) + assert.Equal(t, setting.WebBannerType{}, banner) + + req = NewRequestWithValues(t, "POST", "/-/admin/config", map[string]string{ + "key": "instance.web_banner", + "value": `{"DisplayEnabled":true,"ContentMessage":"test-msg","StartTimeUnix":123,"EndTimeUnix":456}`, + }) + session.MakeRequest(t, req, http.StatusOK) + banner, rev2, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context()) + assert.NotEqual(t, rev1, rev2) + assert.True(t, has) + assert.Equal(t, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "test-msg", + StartTimeUnix: 123, + EndTimeUnix: 456, + }, banner) + }) } diff --git a/tests/integration/config_instance_test.go b/tests/integration/config_instance_test.go new file mode 100644 index 00000000000..c9aa7ab745b --- /dev/null +++ b/tests/integration/config_instance_test.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + "time" + + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/setting/config" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockSystemConfig[T any](t *testing.T, opt *config.Option[T], v T) func() { + jsonBuf, _ := json.Marshal(v) + old := opt.Value(t.Context()) + require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)})) + config.GetDynGetter().InvalidateCache() + return func() { + jsonBuf, _ := json.Marshal(old) + require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)})) + config.GetDynGetter().InvalidateCache() + } +} + +func TestInstance(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("WebBanner", func(t *testing.T) { + t.Run("Visibility", func(t *testing.T) { + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Planned **upgrade** in progress.", + })() + + t.Run("AnonymousUserSeesBanner", func(t *testing.T) { + resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + }) + + t.Run("NormalUserSeesBanner", func(t *testing.T) { + sess := loginUser(t, "user2") + resp := sess.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + }) + + t.Run("AdminSeesBannerWithoutEditHint", func(t *testing.T) { + sess := loginUser(t, "user1") + resp := sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK) + assert.Contains(t, resp.Body.String(), "Planned upgrade in progress.") + assert.NotContains(t, resp.Body.String(), "Edit this banner") + }) + + t.Run("APIRequestUnchanged", func(t *testing.T) { + MakeRequest(t, NewRequest(t, "GET", "/api/v1/version"), http.StatusOK) + }) + }) + + t.Run("TimeWindow", func(t *testing.T) { + now := time.Now().Unix() + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Future banner", + StartTimeUnix: now + 3600, + EndTimeUnix: now + 7200, + })() + + resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.NotContains(t, resp.Body.String(), "Future banner") + + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "Expired banner", + StartTimeUnix: now - 7200, + EndTimeUnix: now - 3600, + })() + + resp = MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK) + assert.NotContains(t, resp.Body.String(), "Expired banner") + }) + }) + + t.Run("MaintenanceMode", func(t *testing.T) { + defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{ + DisplayEnabled: true, + ContentMessage: "MaintenanceModeBanner", + })() + defer mockSystemConfig(t, setting.Config().Instance.MaintenanceMode, setting.MaintenanceModeType{AdminWebAccessOnly: true})() + + t.Run("AnonymousUser", func(t *testing.T) { + req := NewRequest(t, "GET", "/") + req.Header.Add("Accept", "text/html") + resp := MakeRequest(t, req, http.StatusServiceUnavailable) + assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner") + assert.Contains(t, resp.Body.String(), `href="/user/login"`) // it must contain the login link + + MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK) + MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusSeeOther) + MakeRequest(t, NewRequest(t, "GET", "/api/internal/dummy"), http.StatusForbidden) + }) + + t.Run("AdminLogin", func(t *testing.T) { + req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{"user_name": "user1", "password": userPassword}) + resp := MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/-/admin", resp.Header().Get("Location")) + + sess := loginUser(t, "user1") + req = NewRequest(t, "GET", "/") + req.Header.Add("Accept", "text/html") + resp = sess.MakeRequest(t, req, http.StatusServiceUnavailable) + assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner") + + resp = sess.MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusSeeOther) + assert.Equal(t, "/-/admin", resp.Header().Get("Location")) + + sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK) + }) + }) +} diff --git a/web_src/css/admin.css b/web_src/css/admin.css index cda38c6dddf..d84aa7e811b 100644 --- a/web_src/css/admin.css +++ b/web_src/css/admin.css @@ -49,3 +49,11 @@ gap: 1rem; margin-bottom: 1rem; } + +.web-banner-content-editor .render-content.render-preview { + /* use the styles from ".ui.message" */ + padding: 1em 1.5em; + border: 1px solid var(--color-info-border); + background: var(--color-info-bg); + color: var(--color-info-text); +} diff --git a/web_src/css/modules/container.css b/web_src/css/modules/container.css index 236cb986fd0..1b2a1d64b76 100644 --- a/web_src/css/modules/container.css +++ b/web_src/css/modules/container.css @@ -14,3 +14,21 @@ .ui.container.medium-width { width: 800px; } + +.ui.message.web-banner-container { + position: relative; + margin: 0; + border-radius: 0; +} + +.ui.message.web-banner-container > .web-banner-content { + width: 1280px; + max-width: calc(100% - calc(2 * var(--page-margin-x))); + margin: auto; +} + +.ui.message.web-banner-container > button.dismiss-banner { + position: absolute; + right: 20px; + top: 15px; +} diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts new file mode 100644 index 00000000000..e44ccb2a940 --- /dev/null +++ b/web_src/js/features/admin/config.test.ts @@ -0,0 +1,41 @@ +import {ConfigFormValueMapper} from './config.ts'; + +test('ConfigFormValueMapper', () => { + document.body.innerHTML = ` + + + + + + + + + + + + + + + +`; + + const form = document.querySelector('form')!; + const mapper = new ConfigFormValueMapper(form); + mapper.fillFromSystemConfig(); + const formData = mapper.collectToFormData(); + const result: Record = {}; + const keys = [], values = []; + for (const [key, value] of formData.entries()) { + if (key === 'key') keys.push(value as string); + if (key === 'value') values.push(value as string); + } + for (let i = 0; i < keys.length; i++) { + result[keys[i]] = values[i]; + } + expect(result).toEqual({ + 'k1': 'true', + 'k2': '"k2-val"', + 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend + 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', + }); +}); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 76f7c1db50e..047c1a46a44 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -1,24 +1,210 @@ import {showTemporaryTooltip} from '../../modules/tippy.ts'; import {POST} from '../../modules/fetch.ts'; +import {registerGlobalInitFunc} from '../../modules/observer.ts'; +import {queryElems} from '../../utils/dom.ts'; +import {submitFormFetchAction} from '../common-fetch-action.ts'; const {appSubUrl} = window.config; -export function initAdminConfigs(): void { - const elAdminConfig = document.querySelector('.page-content.admin.config'); - if (!elAdminConfig) return; +function initSystemConfigAutoCheckbox(el: HTMLInputElement) { + el.addEventListener('change', async () => { + // if the checkbox is inside a form, we assume it's handled by the form submit and do not send an individual request + if (el.closest('form')) return; + try { + const resp = await POST(`${appSubUrl}/-/admin/config`, { + data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), + }); + const json: Record = await resp.json(); + if (json.errorMessage) throw new Error(json.errorMessage); + } catch (ex) { + showTemporaryTooltip(el, ex.toString()); + el.checked = !el.checked; + } + }); +} - for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) { - el.addEventListener('change', async () => { +type GeneralFormFieldElement = HTMLInputElement; + +function unsupportedElement(el: Element): never { + // HINT: for future developers: if you need to handle a config that cannot be directly mapped to a form element, you should either: + // * Add a "hidden" input to store the value (not configurable) + // * Design a new "component" to handle the config + throw new Error(`Unsupported config form value mapping for ${el.nodeName} (name=${(el as HTMLInputElement).name},type=${(el as HTMLInputElement).type}), please add more and design carefully`); +} + +function requireExplicitValueType(el: Element): never { + throw new Error(`Unsupported config form value type for ${el.nodeName} (name=${(el as HTMLInputElement).name},type=${(el as HTMLInputElement).type}), please add explicit value type with "data-config-value-type" attribute`); +} + +// try to extract the subKey for the config value from the element name +// * return '' if the element name exactly matches the config key, which means the value is directly stored in the element +// * return null if the config key not match +function extractElemConfigSubKey(el: GeneralFormFieldElement, dynKey: string): string | null { + if (el.name === dynKey) return ''; + if (el.name.startsWith(`${dynKey}.`)) return el.name.slice(dynKey.length + 1); // +1 for the dot + return null; +} + +// Due to the different design between HTML form elements and the JSON struct of the config values, we need to explicitly define some types. +// * checkbox can be used for boolean value, it can also be used for multiple values (array) +type ConfigValueType = 'boolean' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. + +function toDatetimeLocalValue(unixSeconds: number) { + const d = new Date(unixSeconds * 1000); + return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); +} + +export class ConfigFormValueMapper { + form: HTMLFormElement; + presetJsonValues: Record = {}; + presetValueTypes: Record = {}; + + constructor(form: HTMLFormElement) { + this.form = form; + for (const el of queryElems(form, '[data-config-value-json]')) { + const dynKey = el.getAttribute('data-config-dyn-key')!; + const jsonStr = el.getAttribute('data-config-value-json'); try { - const resp = await POST(`${appSubUrl}/-/admin/config`, { - data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), - }); - const json: Record = await resp.json(); - if (json.errorMessage) throw new Error(json.errorMessage); - } catch (ex) { - showTemporaryTooltip(el, ex.toString()); - el.checked = !el.checked; + this.presetJsonValues[dynKey] = JSON.parse(jsonStr || '{}'); // empty string also is valid, default to an empty object + } catch (error) { + this.presetJsonValues[dynKey] = {}; // in case the value in database is corrupted, don't break the whole form + console.error(`Error parsing JSON for config ${dynKey}:`, error); } - }); + } + for (const el of queryElems(form, '[data-config-value-type]')) { + const valKey = el.getAttribute('data-config-dyn-key') || el.name; + this.presetValueTypes[valKey] = el.getAttribute('data-config-value-type')! as ConfigValueType; + } + } + + // try to assign the config value to the form element, return true if assigned successfully, + // otherwise return false (e.g. the element is not related to the config key) + assignConfigValueToFormElement(el: GeneralFormFieldElement, dynKey: string, cfgVal: any) { + const subKey = extractElemConfigSubKey(el, dynKey); + if (subKey === null) return false; // if not match, skip + + const val = subKey ? cfgVal![subKey] : cfgVal; + if (val === null) return true; // if name matches, but no value to assign, also succeed because the form element does exist + const valType = this.presetValueTypes[el.name]; + if (el.matches('[type="checkbox"]')) { + if (valType !== 'boolean') requireExplicitValueType(el); + el.checked = Boolean(val ?? el.checked); + } else if (el.matches('[type="datetime-local"]')) { + if (valType !== 'timestamp') requireExplicitValueType(el); + if (val) el.value = toDatetimeLocalValue(val); + } else if (el.matches('textarea')) { + el.value = String(val ?? el.value); + } else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') { + el.value = String(val ?? el.value); + } else { + unsupportedElement(el); + } + return true; + } + + collectConfigValueFromElement(el: GeneralFormFieldElement, _oldVal: any = null) { + let val: any; + const valType = this.presetValueTypes[el.name]; + if (el.matches('[type="checkbox"]')) { + if (valType !== 'boolean') requireExplicitValueType(el); + val = el.checked; + // oldVal: for future use when we support array value with checkbox + } else if (el.matches('[type="datetime-local"]')) { + if (valType !== 'timestamp') requireExplicitValueType(el); + val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null. + } else if (el.matches('textarea')) { + val = el.value; + } else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') { + val = el.value; + } else { + unsupportedElement(el); + } + return val; + } + + collectConfigSubValues(namedElems: Array, dynKey: string, cfgVal: Record) { + for (let idx = 0; idx < namedElems.length; idx++) { + const el = namedElems[idx]; + if (!el) continue; + const subKey = extractElemConfigSubKey(el, dynKey); + if (!subKey) continue; // if not match, skip + cfgVal[subKey] = this.collectConfigValueFromElement(el, cfgVal[subKey]); + namedElems[idx] = null; + } + } + + fillFromSystemConfig() { + for (const [dynKey, cfgVal] of Object.entries(this.presetJsonValues)) { + const elems = this.form.querySelectorAll(`[name^="${CSS.escape(dynKey)}"]`); + let assigned = false; + for (const el of elems) { + if (this.assignConfigValueToFormElement(el, dynKey, cfgVal)) { + assigned = true; + } + } + if (!assigned) throw new Error(`Could not find form element for config ${dynKey}, please check the form design and json struct`); + } + } + + // TODO: OPEN-WITH-EDITOR-APP-JSON: need to use the same logic as backend + marshalConfigValueOpenWithEditorApps(cfgVal: string): string { + const apps: Array<{DisplayName: string, OpenURL: string}> = []; + const lines = cfgVal.split('\n'); + for (const line of lines) { + let [displayName, openUrl] = line.split('=', 2); + displayName = displayName.trim(); + openUrl = openUrl?.trim() ?? ''; + if (!displayName || !openUrl) continue; + apps.push({DisplayName: displayName, OpenURL: openUrl}); + } + return JSON.stringify(apps); + } + + marshalConfigValue(dynKey: string, cfgVal: any): string { + if (dynKey === 'repository.open-with.editor-apps') return this.marshalConfigValueOpenWithEditorApps(cfgVal); + return JSON.stringify(cfgVal); + } + + collectToFormData(): FormData { + const namedElems: Array = []; + queryElems(this.form, '[name]', (el) => namedElems.push(el as GeneralFormFieldElement)); + + // first, process the config options with sub values, for example: + // merge "foo.bar.Enabled", "foo.bar.Message" to "foo.bar" + const formData = new FormData(); + for (const [dynKey, cfgVal] of Object.entries(this.presetJsonValues)) { + this.collectConfigSubValues(namedElems, dynKey, cfgVal); + formData.append('key', dynKey); + formData.append('value', this.marshalConfigValue(dynKey, cfgVal)); + } + + // now, the namedElems should only contain the config options without sub values, + // directly store the value in formData with key as the element name, for example: + for (const el of namedElems) { + if (!el) continue; + const dynKey = el.name; + const newVal = this.collectConfigValueFromElement(el); + formData.append('key', dynKey); + formData.append('value', this.marshalConfigValue(dynKey, newVal)); + } + return formData; } } + +function initSystemConfigForm(form: HTMLFormElement) { + const formMapper = new ConfigFormValueMapper(form); + formMapper.fillFromSystemConfig(); + form.addEventListener('submit', async (e) => { + if (!form.reportValidity()) return; + e.preventDefault(); + const formData = formMapper.collectToFormData(); + await submitFormFetchAction(form, {formData}); + }); +} + +export function initAdminConfigs(): void { + registerGlobalInitFunc('initAdminConfigSettings', (el) => { + queryElems(el, 'input[type="checkbox"][data-config-dyn-key]', initSystemConfigAutoCheckbox); + queryElems(el, 'form.system-config-form', initSystemConfigForm); + }); +} diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 0714de95c8b..0d72fb32c9c 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -67,10 +67,15 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R async function onFormFetchActionSubmit(formEl: HTMLFormElement, e: SubmitEvent) { e.preventDefault(); - await submitFormFetchAction(formEl, submitEventSubmitter(e)); + await submitFormFetchAction(formEl, {formSubmitter: submitEventSubmitter(e)}); } -export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitter?: HTMLElement) { +type SubmitFormFetchActionOpts = { + formSubmitter?: HTMLElement; + formData?: FormData; +}; + +export async function submitFormFetchAction(formEl: HTMLFormElement, opts: SubmitFormFetchActionOpts = {}) { if (formEl.classList.contains('is-loading')) return; formEl.classList.add('is-loading'); @@ -80,8 +85,8 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt const formMethod = formEl.getAttribute('method') || 'get'; const formActionUrl = formEl.getAttribute('action') || window.location.href; - const formData = new FormData(formEl); - const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; + const formData = opts.formData ?? new FormData(formEl); + const [submitterName, submitterValue] = [opts.formSubmitter?.getAttribute('name'), opts.formSubmitter?.getAttribute('value')]; if (submitterName) { formData.append(submitterName, submitterValue || ''); } diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 42104947df8..5b470ea03d5 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -266,8 +266,8 @@ export class ComboMarkdownEditor { addTableButton.addEventListener('click', () => addTablePanelTippy.show()); addTablePanel.querySelector('.ui.button.primary')!.addEventListener('click', () => { - let rows = parseInt(addTablePanel.querySelector('[name=rows]')!.value); - let cols = parseInt(addTablePanel.querySelector('[name=cols]')!.value); + let rows = parseInt(addTablePanel.querySelector('.add-table-rows')!.value); + let cols = parseInt(addTablePanel.querySelector('.add-table-cols')!.value); rows = Math.max(1, Math.min(100, rows)); cols = Math.max(1, Math.min(100, cols)); replaceTextareaSelection(this.textarea, `\n${this.generateMarkdownTable(rows, cols)}\n\n`); diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index b100cd7c914..4957e83d005 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -197,5 +197,5 @@ export function initRepoEditor() { export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { // the content is from the server, so it is safe to use innerHTML - previewPanel.innerHTML = html`
${htmlRaw(htmlContent)}
`; + previewPanel.innerHTML = html`
${htmlRaw(htmlContent)}
`; } From f7f55a356f3910689a9fccc0f3e244c2e08dd196 Mon Sep 17 00:00:00 2001 From: silverwind Date: Thu, 26 Feb 2026 20:13:19 +0100 Subject: [PATCH 04/50] Update tool dependencies and fix new lint issues (#36702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Update golangci-lint v2.9.0 → v2.10.1, misspell v0.7.0 → v0.8.0, actionlint v1.7.10 → v1.7.11 - Fix 20 new QF1012 staticcheck findings by using `fmt.Fprintf` instead of `WriteString(fmt.Sprintf(...))` - Fix SA1019: replace deprecated `ecdsa.PublicKey` field access with `PublicKey.Bytes()` for JWK encoding, with SEC 1 validation and curve derived from signing algorithm - Add unit test for `ToJWK()` covering P-256, P-384, and P-521 curves, also verifying correct coordinate padding per RFC 7518 - Remove dead staticcheck linter exclusion for "argument x is overwritten before first use" ## Test plan - [x] `make lint-go` passes with 0 issues - [x] `go test ./services/oauth2_provider/ -run TestECDSASigningKeyToJWK` passes for all curves 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 --- .golangci.yml | 3 - Makefile | 6 +- models/repo/repo.go | 2 +- modules/git/foreachref/format.go | 2 +- routers/web/repo/setting/lfs.go | 4 +- services/gitdiff/gitdiff.go | 8 +-- services/oauth2_provider/jwtsigningkey.go | 11 +++- .../oauth2_provider/jwtsigningkey_test.go | 61 +++++++++++++++++++ services/release/notes.go | 8 +-- services/webhook/discord.go | 2 +- services/webhook/feishu.go | 2 +- services/webhook/matrix.go | 4 +- services/webhook/msteams.go | 4 +- services/webhook/slack.go | 2 +- services/webhook/telegram.go | 2 +- services/webhook/wechatwork.go | 4 +- 16 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 services/oauth2_provider/jwtsigningkey_test.go diff --git a/.golangci.yml b/.golangci.yml index 2b85c89fdce..4e01169dc68 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -141,9 +141,6 @@ linters: - linters: - unused text: (?i)swagger - - linters: - - staticcheck - text: (?i)argument x is overwritten before first use - linters: - gocritic text: '(?i)commentFormatting: put a space between `//` and comment text' diff --git a/Makefile b/Makefile index cb7742c5c74..d8fce11ee23 100644 --- a/Makefile +++ b/Makefile @@ -15,13 +15,13 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.9.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.10.1 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10 +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest diff --git a/models/repo/repo.go b/models/repo/repo.go index 07b9bf30ccd..7b7f5adb413 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -281,7 +281,7 @@ func (repo *Repository) SizeDetailsString() string { var str strings.Builder sizeDetails := repo.SizeDetails() for _, detail := range sizeDetails { - str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size)) } return strings.TrimSuffix(str.String(), ", ") } diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index d2f9998fe81..cee21c5b668 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -53,7 +53,7 @@ func (f Format) Flag() string { var formatFlag strings.Builder for i, field := range f.fieldNames { // field key and field value - formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field)) + fmt.Fprintf(&formatFlag, "%s %%(%s)", field, field) if i < len(f.fieldNames)-1 { // note: escape delimiters to allow control characters as diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 8a8015035f4..a3a60963d43 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -301,13 +301,13 @@ func LFSFileGet(ctx *context.Context) { if index != len(lines)-1 { line += "\n" } - output.WriteString(fmt.Sprintf(`
  • %s
  • `, index+1, index+1, line)) + fmt.Fprintf(&output, `
  • %s
  • `, index+1, index+1, line) } ctx.Data["FileContent"] = gotemplate.HTML(output.String()) output.Reset() for i := 0; i < len(lines); i++ { - output.WriteString(fmt.Sprintf(`%d`, i+1, i+1)) + fmt.Fprintf(&output, `%d`, i+1, i+1) } ctx.Data["LineNums"] = gotemplate.HTML(output.String()) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 7777cf4a1c3..b23e5b1b1cc 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1594,10 +1594,10 @@ func generatePatchForUnchangedLineFromReader(reader io.Reader, treePath string, // Generate synthetic patch var patchBuilder strings.Builder - patchBuilder.WriteString(fmt.Sprintf("diff --git a/%s b/%s\n", treePath, treePath)) - patchBuilder.WriteString(fmt.Sprintf("--- a/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("+++ b/%s\n", treePath)) - patchBuilder.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines))) + fmt.Fprintf(&patchBuilder, "diff --git a/%s b/%s\n", treePath, treePath) + fmt.Fprintf(&patchBuilder, "--- a/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "+++ b/%s\n", treePath) + fmt.Fprintf(&patchBuilder, "@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines)) for _, lineContent := range lines { patchBuilder.WriteString(" ") diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 03c7403f75c..4898d54166a 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -214,13 +214,20 @@ func (key ecdsaSingingKey) VerifyKey() any { func (key ecdsaSingingKey) ToJWK() (map[string]string, error) { pubKey := key.key.Public().(*ecdsa.PublicKey) + // PublicKey.Bytes returns the uncompressed SEC 1 format: 0x04 || X || Y + pubKeyBytes, err := pubKey.Bytes() + if err != nil { + return nil, err + } + + coordLen := (len(pubKeyBytes) - 1) / 2 return map[string]string{ "kty": "EC", "alg": key.SigningMethod().Alg(), "kid": key.id, "crv": pubKey.Params().Name, - "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), - "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), + "x": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1 : 1+coordLen]), + "y": base64.RawURLEncoding.EncodeToString(pubKeyBytes[1+coordLen:]), }, nil } diff --git a/services/oauth2_provider/jwtsigningkey_test.go b/services/oauth2_provider/jwtsigningkey_test.go new file mode 100644 index 00000000000..55de81dfd88 --- /dev/null +++ b/services/oauth2_provider/jwtsigningkey_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package oauth2_provider + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "math/big" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestECDSASigningKeyToJWK(t *testing.T) { + for _, tc := range []struct { + curve elliptic.Curve + signingMethod jwt.SigningMethod + expectedAlg string + expectedCrv string + coordLen int + }{ + {elliptic.P256(), jwt.SigningMethodES256, "ES256", "P-256", 32}, + {elliptic.P384(), jwt.SigningMethodES384, "ES384", "P-384", 48}, + {elliptic.P521(), jwt.SigningMethodES512, "ES512", "P-521", 66}, + } { + t.Run(tc.expectedCrv, func(t *testing.T) { + privKey, err := ecdsa.GenerateKey(tc.curve, rand.Reader) + require.NoError(t, err) + + signingKey, err := newECDSASingingKey(tc.signingMethod, privKey) + require.NoError(t, err) + + jwk, err := signingKey.ToJWK() + require.NoError(t, err) + + assert.Equal(t, "EC", jwk["kty"]) + assert.Equal(t, tc.expectedAlg, jwk["alg"]) + assert.Equal(t, tc.expectedCrv, jwk["crv"]) + assert.NotEmpty(t, jwk["kid"]) + + // Verify coordinates are the correct fixed length per RFC 7518 / SEC 1 + xBytes, err := base64.RawURLEncoding.DecodeString(jwk["x"]) + require.NoError(t, err) + assert.Len(t, xBytes, tc.coordLen) + + yBytes, err := base64.RawURLEncoding.DecodeString(jwk["y"]) + require.NoError(t, err) + assert.Len(t, yBytes, tc.coordLen) + + // Verify the decoded coordinates reconstruct the original public key point + pubKey := privKey.Public().(*ecdsa.PublicKey) + assert.Equal(t, 0, new(big.Int).SetBytes(xBytes).Cmp(pubKey.X)) + assert.Equal(t, 0, new(big.Int).SetBytes(yBytes).Cmp(pubKey.Y)) + }) + } +} diff --git a/services/release/notes.go b/services/release/notes.go index c9dc75af70d..92ee22e6043 100644 --- a/services/release/notes.go +++ b/services/release/notes.go @@ -113,7 +113,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, for _, pr := range prs { prURL := pr.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL) } builder.WriteString("\n") @@ -121,7 +121,7 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, if len(contributors) > 0 { builder.WriteString("## Contributors\n") for _, contributor := range contributors { - builder.WriteString(fmt.Sprintf("* @%s\n", contributor.Name)) + fmt.Fprintf(&builder, "* @%s\n", contributor.Name) } builder.WriteString("\n") } @@ -130,14 +130,14 @@ func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, builder.WriteString("## New Contributors\n") for _, contributor := range newContributors { prURL := contributor.Issue.HTMLURL(ctx) - builder.WriteString(fmt.Sprintf("* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL)) + fmt.Fprintf(&builder, "* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL) } builder.WriteString("\n") } builder.WriteString("**Full Changelog**: ") compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName)) - builder.WriteString(fmt.Sprintf("[%s...%s](%s)", baseRef, tagName, compareURL)) + fmt.Fprintf(&builder, "[%s...%s](%s)", baseRef, tagName, compareURL) builder.WriteByte('\n') return builder.String() } diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 19af779120d..c0af7c02433 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -169,7 +169,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) { if utf8.RuneCountInString(message) > 50 { message = fmt.Sprintf("%.47s...", message) } - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n") diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index ecce9acc436..ac581df85a3 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -77,7 +77,7 @@ func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) { ) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)) + fmt.Fprintf(&text, "[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc) // for each commit, generate attachment text for i, commit := range p.Commits { var authorName string diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 63fbbf40a96..fa01ecd0b19 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -174,11 +174,11 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) { repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName) branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref) var text strings.Builder - text.WriteString(fmt.Sprintf("[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink)) + fmt.Fprintf(&text, "[%s] %s pushed %s to %s:
    ", repoLink, p.Pusher.UserName, commitDesc, branchLink) // for each commit, generate a new line text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)) + fmt.Fprintf(&text, "%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("
    ") diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index fa39e7228e3..34db9037121 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -134,8 +134,8 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) { var text strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, - strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)) + fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL, + strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name) // add linebreak to each commit but the last if i < len(p.Commits)-1 { text.WriteString("\n\n") diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 0b3dda467cc..94d41d21790 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -211,7 +211,7 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) { var attachmentText strings.Builder // for each commit, generate attachment text for i, commit := range p.Commits { - attachmentText.WriteString(fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))) + fmt.Fprintf(&attachmentText, "%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)) // add linebreak to each commit but the last if i < len(p.Commits)-1 { attachmentText.WriteString("\n") diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index 2abc743fabd..8e9a53a5de5 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -96,7 +96,7 @@ func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) { var htmlCommits strings.Builder for _, commit := range p.Commits { - htmlCommits.WriteString(fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n")))) + fmt.Fprintf(&htmlCommits, "\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))) if commit.Author != nil { htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name)) } diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index da9c6b584c2..cac6a700c02 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -86,8 +86,8 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error } message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n") - text.WriteString(fmt.Sprintf(" > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, - message, authorName)) + fmt.Fprintf(&text, " > [%s](%s) \r\n >%s \n >%s", commit.ID[:7], commit.URL, + message, authorName) // add linebreak to each commit but the last if i < len(p.Commits)-1 { From f9a2a8ae8df81fa5bf23ede004cc51a36d01c53a Mon Sep 17 00:00:00 2001 From: WinterCabbage <54889338+WinterCabbage@users.noreply.github.com> Date: Fri, 27 Feb 2026 03:58:10 +0800 Subject: [PATCH 05/50] Fix milestone/project text overflow in issue sidebar (#36741) Fixes #36732 Co-authored-by: Giteabot --- templates/repo/issue/sidebar/milestone_list.tmpl | 8 ++++---- templates/repo/issue/sidebar/project_list.tmpl | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/templates/repo/issue/sidebar/milestone_list.tmpl b/templates/repo/issue/sidebar/milestone_list.tmpl index 5bc961ed3c2..1442963c932 100644 --- a/templates/repo/issue/sidebar/milestone_list.tmpl +++ b/templates/repo/issue/sidebar/milestone_list.tmpl @@ -18,14 +18,14 @@ {{svg "octicon-search"}}
    - {{end}} - + >
    T
    {{if .RefFullName.IsBranch}} {{$addFilePath := .TreePath}} diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 5e26bcc6853..95004096d8a 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import ActivityHeatmap from '../components/ActivityHeatmap.vue'; import {translateMonth, translateDay} from '../utils.ts'; import {GET} from '../modules/fetch.ts'; @@ -46,6 +45,7 @@ export async function initHeatmap() { noDataText: el.getAttribute('data-locale-no-contributions'), }; + const {default: ActivityHeatmap} = await import(/* webpackChunkName: "ActivityHeatmap" */ '../components/ActivityHeatmap.vue'); const View = createApp(ActivityHeatmap, {values, locale}); View.mount(el); el.classList.remove('is-loading'); diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 7a35a3c7ffe..8d306b2bab8 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import RepoFileSearch from '../components/RepoFileSearch.vue'; import {registerGlobalInitFunc} from '../modules/observer.ts'; const threshold = 50; @@ -69,7 +68,8 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { } export function initRepoFileSearch() { - registerGlobalInitFunc('initRepoFileSearch', (el) => { + registerGlobalInitFunc('initRepoFileSearch', async (el) => { + const {default: RepoFileSearch} = await import(/* webpackChunkName: "RepoFileSearch" */ '../components/RepoFileSearch.vue'); createApp(RepoFileSearch, { repoLink: el.getAttribute('data-repo-link'), currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), diff --git a/web_src/js/features/repo-issue-pull.ts b/web_src/js/features/repo-issue-pull.ts index 91f27305bdd..093f484b42c 100644 --- a/web_src/js/features/repo-issue-pull.ts +++ b/web_src/js/features/repo-issue-pull.ts @@ -1,5 +1,4 @@ import {createApp} from 'vue'; -import PullRequestMergeForm from '../components/PullRequestMergeForm.vue'; import {GET, POST} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {createElementFromHTML} from '../utils/dom.ts'; @@ -63,10 +62,11 @@ function initRepoPullRequestCommitStatus(el: HTMLElement) { } } -function initRepoPullRequestMergeForm(box: HTMLElement) { +async function initRepoPullRequestMergeForm(box: HTMLElement) { const el = box.querySelector('#pull-request-merge-form'); if (!el) return; + const {default: PullRequestMergeForm} = await import(/* webpackChunkName: "PullRequestMergeForm" */ '../components/PullRequestMergeForm.vue'); const view = createApp(PullRequestMergeForm); view.mount(el); } diff --git a/web_src/js/markup/refissue.ts b/web_src/js/markup/refissue.ts index ff6fdd624f0..f2fcd24f39d 100644 --- a/web_src/js/markup/refissue.ts +++ b/web_src/js/markup/refissue.ts @@ -1,7 +1,6 @@ import {queryElems} from '../utils/dom.ts'; import {parseIssueHref} from '../utils.ts'; import {createApp} from 'vue'; -import ContextPopup from '../components/ContextPopup.vue'; import {createTippy, getAttachedTippyInstance} from '../modules/tippy.ts'; export function initMarkupRefIssue(el: HTMLElement) { @@ -20,6 +19,14 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { if (!issuePathInfo.ownerName) return; const el = document.createElement('div'); + const onShowAsync = async () => { + const {default: ContextPopup} = await import(/* webpackChunkName: "ContextPopup" */ '../components/ContextPopup.vue'); + const view = createApp(ContextPopup, { + // backend: GetIssueInfo + loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, + }); + view.mount(el); + }; const tippy = createTippy(refIssue, { theme: 'default', content: el, @@ -29,13 +36,7 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { role: 'dialog', interactiveBorder: 5, // onHide() { return false }, // help to keep the popup and debug the layout - onShow: () => { - const view = createApp(ContextPopup, { - // backend: GetIssueInfo - loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, - }); - view.mount(el); - }, + onShow: () => { onShowAsync() }, }); tippy.show(); } From 619db646f54dc1844cad94c1ac801dcd66f7a746 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 27 Feb 2026 20:38:44 +0800 Subject: [PATCH 09/50] Deprecate RenderWithErr (#36769) --- routers/install/install.go | 68 ++++++++++----------- routers/web/admin/auths.go | 16 ++--- routers/web/admin/users.go | 42 ++++++------- routers/web/auth/2fa.go | 4 +- routers/web/auth/auth.go | 28 ++++----- routers/web/auth/linkaccount.go | 10 +-- routers/web/auth/openid.go | 24 ++++---- routers/web/auth/password.go | 24 ++++---- routers/web/org/org.go | 8 +-- routers/web/org/setting.go | 2 +- routers/web/org/teams.go | 8 +-- routers/web/repo/migrate.go | 40 ++++++------ routers/web/repo/milestone.go | 4 +- routers/web/repo/release.go | 14 ++--- routers/web/repo/repo.go | 20 +++--- routers/web/repo/setting/deploy_key.go | 8 +-- routers/web/repo/setting/setting.go | 58 +++++++++--------- routers/web/repo/wiki.go | 6 +- routers/web/user/setting/account.go | 12 ++-- routers/web/user/setting/keys.go | 16 ++--- routers/web/user/setting/security/openid.go | 10 +-- services/context/captcha.go | 2 +- services/context/context_response.go | 8 ++- 23 files changed, 218 insertions(+), 214 deletions(-) diff --git a/routers/install/install.go b/routers/install/install.go index 399128b6ed4..1a60fee3397 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -141,7 +141,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { if (setting.Database.Type == "sqlite3") && len(setting.Database.Path) == 0 { ctx.Data["Err_DbPath"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_db_path"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form) return false } @@ -152,10 +152,10 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { if err = db.InitEngine(ctx); err != nil { if strings.Contains(err.Error(), `Unknown database type: sqlite3`) { ctx.Data["Err_DbType"] = true - ctx.RenderWithErr(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form) } else { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) } return false } @@ -163,20 +163,20 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { err = db_install.CheckDatabaseConnection(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form) return false } hasPostInstallationUser, err := db_install.HasPostInstallationUsers(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form) return false } dbMigrationVersion, err := db_install.GetMigrationVersion(ctx) if err != nil { ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form) return false } @@ -185,7 +185,7 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) bool { confirmed := form.ReinstallConfirmFirst && form.ReinstallConfirmSecond && form.ReinstallConfirmThird if !confirmed { ctx.Data["Err_DbInstalledBefore"] = true - ctx.RenderWithErr(ctx.Tr("install.reinstall_error"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form) return false } @@ -225,7 +225,7 @@ func SubmitInstall(ctx *context.Context) { } if _, err = exec.LookPath("git"); err != nil { - ctx.RenderWithErr(ctx.Tr("install.test_git_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, &form) return } @@ -248,7 +248,7 @@ func SubmitInstall(ctx *context.Context) { // Prepare AppDataPath, it is very important for Gitea if err = setting.PrepareAppDataPath(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form) return } @@ -256,7 +256,7 @@ func SubmitInstall(ctx *context.Context) { form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/") if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil { ctx.Data["Err_RepoRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form) return } @@ -265,7 +265,7 @@ func SubmitInstall(ctx *context.Context) { form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/") if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil { ctx.Data["Err_LFSRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form) return } } @@ -274,14 +274,14 @@ func SubmitInstall(ctx *context.Context) { form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/") if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil { ctx.Data["Err_LogRootPath"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form) return } currentUser, match := setting.IsRunUserMatchCurrentUser(form.RunUser) if !match { ctx.Data["Err_RunUser"] = true - ctx.RenderWithErr(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.run_user_not_match", form.RunUser, currentUser), tplInstall, &form) return } @@ -289,7 +289,7 @@ func SubmitInstall(ctx *context.Context) { if form.DisableRegistration && len(form.AdminName) == 0 { ctx.Data["Err_Services"] = true ctx.Data["Err_Admin"] = true - ctx.RenderWithErr(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form) return } @@ -300,33 +300,33 @@ func SubmitInstall(ctx *context.Context) { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminName"] = true if db.IsErrNameReserved(err) { - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) return } else if db.IsErrNamePatternNotAllowed(err) { - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) return } - ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) return } // Check Admin email if len(form.AdminEmail) == 0 { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminEmail"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) return } // Check admin password. if len(form.AdminPasswd) == 0 { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) return } if form.AdminPasswd != form.AdminConfirmPasswd { ctx.Data["Err_Admin"] = true ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplInstall, form) return } } @@ -335,7 +335,7 @@ func SubmitInstall(ctx *context.Context) { if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil { db.UnsetDefaultEngine() ctx.Data["Err_DbSetting"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form) return } @@ -379,7 +379,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath) var lfsJwtSecret string if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form) return } cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret) @@ -389,7 +389,7 @@ func SubmitInstall(ctx *context.Context) { if len(strings.TrimSpace(form.SMTPAddr)) > 0 { if _, err := mail.ParseAddress(form.SMTPFrom); err != nil { - ctx.RenderWithErr(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form) return } @@ -410,7 +410,7 @@ func SubmitInstall(ctx *context.Context) { setting.Config().Picture.DisableGravatar.DynKey(): strconv.FormatBool(form.DisableGravatar), setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), }); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } @@ -443,7 +443,7 @@ func SubmitInstall(ctx *context.Context) { if setting.InternalToken == "" { var internalToken string if internalToken, err = generate.NewInternalToken(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form) return } cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken) @@ -454,7 +454,7 @@ func SubmitInstall(ctx *context.Context) { if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") { _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() if err != nil { - ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) return } cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) @@ -464,7 +464,7 @@ func SubmitInstall(ctx *context.Context) { if setting.SecretKey == "" { var secretKey string if secretKey, err = generate.NewSecretKey(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form) return } cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey) @@ -474,7 +474,7 @@ func SubmitInstall(ctx *context.Context) { var algorithm *hash.PasswordHashAlgorithm setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm) if algorithm == nil { - ctx.RenderWithErr(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form) return } cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm) @@ -484,14 +484,14 @@ func SubmitInstall(ctx *context.Context) { err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm) if err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } setting.EnvironmentToConfig(cfg, os.Environ()) if err = cfg.SaveTo(setting.CustomConf); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } @@ -527,7 +527,7 @@ func SubmitInstall(ctx *context.Context) { setting.InstallLock = false ctx.Data["Err_AdminName"] = true ctx.Data["Err_AdminEmail"] = true - ctx.RenderWithErr(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form) return } log.Info("Admin account already exist") @@ -544,16 +544,16 @@ func SubmitInstall(ctx *context.Context) { // Auto-login for admin if err = ctx.Session.Set("uid", u.ID); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } if err = ctx.Session.Set("uname", u.Name); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } if err = ctx.Session.Release(); err != nil { - ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) return } } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 3407789f2f9..e29aca127c8 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -273,7 +273,7 @@ func NewAuthSourcePost(ctx *context.Context) { discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { ctx.Data["Err_DiscoveryURL"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) return } } @@ -281,13 +281,13 @@ func NewAuthSourcePost(ctx *context.Context) { var err error config, err = parseSSPIConfig(ctx, form) if err != nil { - ctx.RenderWithErr(err.Error(), tplAuthNew, form) + ctx.RenderWithErrDeprecated(err.Error(), tplAuthNew, form) return } existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI}) if err != nil || len(existing) > 0 { ctx.Data["Err_Type"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form) return } default: @@ -311,11 +311,11 @@ func NewAuthSourcePost(ctx *context.Context) { }); err != nil { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) } else if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Data["Err_DiscoveryURL"] = true unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap() - ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) } else { ctx.ServerError("auth.CreateSource", err) } @@ -403,14 +403,14 @@ func EditAuthSourcePost(ctx *context.Context) { discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { ctx.Data["Err_DiscoveryURL"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) return } } case auth.SSPI: config, err = parseSSPIConfig(ctx, form) if err != nil { - ctx.RenderWithErr(err.Error(), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(err.Error(), tplAuthEdit, form) return } default: @@ -426,7 +426,7 @@ func EditAuthSourcePost(ctx *context.Context) { if err := auth.UpdateSource(ctx, source); err != nil { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form) + ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form) } else if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Flash.Error(err.Error(), true) ctx.Data["Err_DiscoveryURL"] = true diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index ed0eecf90a6..19499fdab56 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -151,12 +151,12 @@ func NewUserPost(ctx *context.Context) { if u.LoginType == auth.NoType || u.LoginType == auth.Plain { if len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form) return } if !password.IsComplexEnough(form.Password) { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserNew, &form) return } if err := password.IsPwned(ctx, form.Password); err != nil { @@ -166,7 +166,7 @@ func NewUserPost(ctx *context.Context) { log.Error(err.Error()) errMsg = ctx.Tr("auth.password_pwned_err") } - ctx.RenderWithErr(errMsg, tplUserNew, &form) + ctx.RenderWithErrDeprecated(errMsg, tplUserNew, &form) return } u.MustChangePassword = form.MustChangePassword @@ -176,22 +176,22 @@ func NewUserPost(ctx *context.Context) { switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserNew, &form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserNew, &form) case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserNew, &form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form) default: ctx.ServerError("CreateUser", err) } @@ -349,19 +349,19 @@ func EditUserPost(ctx *context.Context) { switch { case user_model.IsErrUserIsNotLocal(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form) case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserEdit, &form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form) default: ctx.ServerError("RenameUser", err) } @@ -392,16 +392,16 @@ func EditUserPost(ctx *context.Context) { switch { case errors.Is(err, password.ErrMinLength): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form) case errors.Is(err, password.ErrComplexity): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form) case errors.Is(err, password.ErrIsPwned): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form) case password.IsErrIsPwnedRequest(err): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form) default: ctx.ServerError("UpdateUser", err) } @@ -413,10 +413,10 @@ func EditUserPost(ctx *context.Context) { switch { case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserEdit, &form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserEdit, &form) default: ctx.ServerError("AddOrSetPrimaryEmailAddress", err) } @@ -444,7 +444,7 @@ func EditUserPost(ctx *context.Context) { if err := user_service.UpdateUser(ctx, u, opts); err != nil { if user_model.IsErrDeleteLastAdminUser(err) { - ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form) } else { ctx.ServerError("UpdateUser", err) } diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go index a19c9d7aca3..73b218d92df 100644 --- a/routers/web/auth/2fa.go +++ b/routers/web/auth/2fa.go @@ -92,7 +92,7 @@ func TwoFactorPost(ctx *context.Context) { return } - ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{}) } // TwoFactorScratch shows the scratch code form for two-factor authentication. @@ -160,5 +160,5 @@ func TwoFactorScratchPost(ctx *context.Context) { return } - ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{}) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 9529525a273..c50a197f51a 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -245,10 +245,10 @@ func SignInPost(ctx *context.Context) { u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password) if err != nil { if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form) log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) } else if user_model.IsErrEmailAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSignIn, &form) log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) } else if user_model.IsErrUserProhibitLogin(err) { log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err) @@ -491,23 +491,23 @@ func SignUpPost(ctx *context.Context) { } if !form.IsEmailDomainAllowed() { - ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form) return } if form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplSignUp, &form) return } if len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form) return } if !password.IsComplexEnough(form.Password) { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplSignUp, &form) return } if err := password.IsPwned(ctx, form.Password); err != nil { @@ -517,7 +517,7 @@ func SignUpPost(ctx *context.Context) { errMsg = ctx.Tr("auth.password_pwned_err") } ctx.Data["Err_Password"] = true - ctx.RenderWithErr(errMsg, tplSignUp, &form) + ctx.RenderWithErrDeprecated(errMsg, tplSignUp, &form) return } @@ -587,25 +587,25 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, switch { case user_model.IsErrUserAlreadyExist(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tpl, form) case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tpl, form) case user_model.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form) case user_model.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form) case db.IsErrNameReserved(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) case db.IsErrNameCharsNotAllowed(err): ctx.Data["Err_UserName"] = true - ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form) default: ctx.ServerError("CreateUser", err) } diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index c624d896ca7..faa712471f4 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -99,10 +99,10 @@ func LinkAccount(ctx *context.Context) { func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl templates.TplName, invoker string, err error) { if errors.Is(err, util.ErrNotExist) { - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) } else if errors.Is(err, util.ErrInvalidArgument) { ctx.Data["user_exists"] = true - ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) + ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm) } else if user_model.IsErrUserProhibitLogin(err) { ctx.Data["user_exists"] = true log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err) @@ -266,7 +266,7 @@ func LinkAccountPostRegister(ctx *context.Context) { } if !form.IsEmailDomainAllowed() { - ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form) return } @@ -280,12 +280,12 @@ func LinkAccountPostRegister(ctx *context.Context) { } else { if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplLinkAccount, &form) return } if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form) return } } diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 948e65366e5..c9843146d45 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -82,7 +82,7 @@ func SignInOpenIDPost(ctx *context.Context) { id, err := openid.Normalize(form.Openid) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form) return } form.Openid = id @@ -91,7 +91,7 @@ func SignInOpenIDPost(ctx *context.Context) { err = allowedOpenIDURI(id) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form) return } @@ -99,7 +99,7 @@ func SignInOpenIDPost(ctx *context.Context) { url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) if err != nil { log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error()) - ctx.RenderWithErr("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form) + ctx.RenderWithErrDeprecated("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form) return } @@ -129,7 +129,7 @@ func signInOpenIDVerify(ctx *context.Context) { id, err := openid.Verify(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -143,7 +143,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, err := user_model.GetUserByOpenID(ctx, id) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -162,14 +162,14 @@ func signInOpenIDVerify(ctx *context.Context) { parsedURL, err := url.Parse(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return } values, err := url.ParseQuery(parsedURL.RawQuery) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -183,7 +183,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, err = user_model.GetUserByEmail(ctx, email) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -199,7 +199,7 @@ func signInOpenIDVerify(ctx *context.Context) { u, _ = user_model.GetUserByName(ctx, nickname) if err != nil { if !user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{ Openid: id, }) return @@ -273,7 +273,7 @@ func ConnectOpenIDPost(ctx *context.Context) { userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} if err = user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form) return } ctx.ServerError("AddUserOpenID", err) @@ -352,7 +352,7 @@ func RegisterOpenIDPost(ctx *context.Context) { length := max(setting.MinPasswordLength, 256) password, err := util.CryptoRandomString(int64(length)) if err != nil { - ctx.RenderWithErr(err.Error(), tplSignUpOID, form) + ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form) return } @@ -370,7 +370,7 @@ func RegisterOpenIDPost(ctx *context.Context) { userOID := &user_model.UserOpenID{UID: u.ID, URI: oid} if err = user_model.AddUserOpenID(ctx, userOID); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form) return } ctx.ServerError("AddUserOpenID", err) diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go index 61c6119470d..11e085f5b1e 100644 --- a/routers/web/auth/password.go +++ b/routers/web/auth/password.go @@ -74,7 +74,7 @@ func ForgotPasswdPost(ctx *context.Context) { if !u.IsLocal() && !u.IsOAuth2() { ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil) return } @@ -171,7 +171,7 @@ func ResetPasswdPost(ctx *context.Context) { if !twofa.VerifyScratchToken(ctx.FormString("token")) { ctx.Data["IsResetForm"] = true ctx.Data["Err_Token"] = true - ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil) return } regenerateScratchToken = true @@ -185,7 +185,7 @@ func ResetPasswdPost(ctx *context.Context) { if !ok || twofa.LastUsedPasscode == passcode { ctx.Data["IsResetForm"] = true ctx.Data["Err_Passcode"] = true - ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil) return } @@ -206,13 +206,13 @@ func ResetPasswdPost(ctx *context.Context) { ctx.Data["Err_Password"] = true switch { case errors.Is(err, password.ErrMinLength): - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil) case errors.Is(err, password.ErrComplexity): - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil) case errors.Is(err, password.ErrIsPwned): - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil) case password.IsErrIsPwnedRequest(err): - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil) default: ctx.ServerError("UpdateAuth", err) } @@ -275,7 +275,7 @@ func MustChangePasswordPost(ctx *context.Context) { if form.Password != form.Retype { ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) return } @@ -287,16 +287,16 @@ func MustChangePasswordPost(ctx *context.Context) { switch { case errors.Is(err, password.ErrMinLength): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) case errors.Is(err, password.ErrComplexity): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form) case errors.Is(err, password.ErrIsPwned): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form) case password.IsErrIsPwnedRequest(err): ctx.Data["Err_Password"] = true - ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form) default: ctx.ServerError("UpdateAuth", err) } diff --git a/routers/web/org/org.go b/routers/web/org/org.go index 0540d5c591e..36978ba65bd 100644 --- a/routers/web/org/org.go +++ b/routers/web/org/org.go @@ -65,13 +65,13 @@ func CreatePost(ctx *context.Context) { ctx.Data["Err_OrgName"] = true switch { case user_model.IsErrUserAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form) case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form) case organization.IsErrUserNotAllowedCreateOrg(err): - ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form) default: ctx.ServerError("CreateOrganization", err) } diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 0e4dab8fb68..04baa58b734 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -73,7 +73,7 @@ func SettingsPost(ctx *context.Context) { if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form) return } } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 0ec7cfddc5f..1e22a670320 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -359,7 +359,7 @@ func NewTeamPost(ctx *context.Context) { } if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { - ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } @@ -367,7 +367,7 @@ func NewTeamPost(ctx *context.Context) { ctx.Data["Err_TeamName"] = true switch { case org_model.IsErrTeamAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("NewTeam", err) } @@ -536,7 +536,7 @@ func EditTeamPost(ctx *context.Context) { } if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 { - ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form) return } @@ -544,7 +544,7 @@ func EditTeamPost(ctx *context.Context) { ctx.Data["Err_TeamName"] = true switch { case org_model.IsErrTeamAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form) default: ctx.ServerError("UpdateTeam", err) } diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index 8f4adb2ad22..bb6f1e6b7eb 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -79,44 +79,44 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, switch { case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form) case migrations.IsTwoFactorAuthError(err): - ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form) case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tpl, form) + ctx.RenderWithErrDeprecated(msg, tpl, form) case repo_model.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) default: err = util.SanitizeErrorCredentialURLs(err) if strings.Contains(err.Error(), "Authentication failed") || strings.Contains(err.Error(), "Bad credentials") || strings.Contains(err.Error(), "could not read Username") { ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form) } else if strings.Contains(err.Error(), "fatal:") { ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) } else { ctx.ServerError(name, err) } @@ -128,24 +128,24 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tpl, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tpl, form) } else { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form) } case addrErr.IsInvalidPath: - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form) default: log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form) } } else { log.Error("Error whilst updating url: %v", err) - ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form) } } @@ -193,7 +193,7 @@ func MigratePost(ctx *context.Context) { ep := lfs.DetermineEndpoint("", form.LFSEndpoint) if ep == nil { ctx.Data["Err_LFSEndpoint"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form) return } err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index dd53b1d3f10..09196d4bc20 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -118,7 +118,7 @@ func NewMilestonePost(ctx *context.Context) { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true - ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } @@ -174,7 +174,7 @@ func EditMilestonePost(ctx *context.Context) { deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline) if err != nil { ctx.Data["Err_Deadline"] = true - ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form) return } diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 1b36dc4d442..891af4c2d5c 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -452,13 +452,13 @@ func NewReleasePost(ctx *context.Context) { } if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist { - ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form) return } if !form.TagOnly && form.Title == "" { // if not "tag only", then the title of the release cannot be empty - ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form) return } @@ -466,13 +466,13 @@ func NewReleasePost(ctx *context.Context) { ctx.Data["Err_TagName"] = true switch { case release_service.IsErrTagAlreadyExists(err): - ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form) case repo_model.IsErrReleaseAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) case release_service.IsErrInvalidTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form) case release_service.IsErrProtectedTagName(err): - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form) default: ctx.ServerError("handleTagReleaseError", err) } @@ -525,7 +525,7 @@ func NewReleasePost(ctx *context.Context) { // add new logic: if tag-only, do not convert the tag to a release if form.TagOnly || !rel.IsTag { ctx.Data["Err_TagName"] = true - ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form) return } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index bc2b0264c0a..6ab9bc03fb1 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -186,28 +186,28 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error, case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tpl, form) + ctx.RenderWithErrDeprecated(msg, tpl, form) case repo_model.IsErrRepoAlreadyExist(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form) } case db.IsErrNameReserved(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form) case db.IsErrNamePatternNotAllowed(err): ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form) default: ctx.ServerError(name, err) } @@ -254,7 +254,7 @@ func CreatePost(ctx *context.Context) { } if !opts.IsValid() { - ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.one_item"), tplCreate, form) return } @@ -264,7 +264,7 @@ func CreatePost(ctx *context.Context) { } if !templateRepo.IsTemplate { - ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.invalid"), tplCreate, form) return } diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go index 193562528bf..ab54b8ccf5a 100644 --- a/routers/web/repo/setting/deploy_key.go +++ b/routers/web/repo/setting/deploy_key.go @@ -76,16 +76,16 @@ func DeployKeysPost(ctx *context.Context) { switch { case asymkey_model.IsErrDeployKeyAlreadyExist(err): ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form) case asymkey_model.IsErrKeyAlreadyExist(err): ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form) case asymkey_model.IsErrKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err): ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form) default: ctx.ServerError("AddDeployKey", err) } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index f9e80a72e02..8475c8e21e2 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -181,23 +181,23 @@ func handleSettingsPostUpdate(ctx *context.Context) { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form) case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) case repo_model.IsErrRepoFilesAlreadyExist(err): ctx.Data["Err_RepoName"] = true switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form) } case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) default: ctx.ServerError("ChangeRepositoryName", err) } @@ -247,7 +247,7 @@ func handleSettingsPostMirror(ctx *context.Context) { interval, err := time.ParseDuration(form.Interval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { ctx.Data["Err_Interval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) return } @@ -298,7 +298,7 @@ func handleSettingsPostMirror(ctx *context.Context) { ep := lfs.DetermineEndpoint("", form.LFSEndpoint) if ep == nil { ctx.Data["Err_LFSEndpoint"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form) return } err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer) @@ -369,7 +369,7 @@ func handleSettingsPostPushMirrorUpdate(ctx *context.Context) { interval, err := time.ParseDuration(form.PushMirrorInterval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{}) return } @@ -445,7 +445,7 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) { interval, err := time.ParseDuration(form.PushMirrorInterval) if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) { ctx.Data["Err_PushMirrorInterval"] = true - ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form) return } @@ -733,7 +733,7 @@ func handleSettingsPostConvert(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -767,7 +767,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -803,14 +803,14 @@ func handleSettingsPostTransfer(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name")) if err != nil { if user_model.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } ctx.ServerError("IsUserExist", err) @@ -820,7 +820,7 @@ func handleSettingsPostTransfer(ctx *context.Context) { if newOwner.Type == user_model.UserTypeOrganization { if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) { // The user shouldn't know about this organization - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) return } } @@ -834,14 +834,14 @@ func handleSettingsPostTransfer(ctx *context.Context) { oldFullname := repo.FullName() if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil { if repo_model.IsErrRepoAlreadyExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) } else if repo_model.IsErrRepoTransferInProgress(err) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil) } else if repo_service.IsRepositoryLimitReached(err) { limit := err.(repo_service.LimitReachedError).Limit - ctx.RenderWithErr(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil) } else if errors.Is(err, user_model.ErrBlockedUser) { - ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil) } else { ctx.ServerError("TransferOwnership", err) } @@ -895,7 +895,7 @@ func handleSettingsPostDelete(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -922,7 +922,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) { return } if repo.Name != form.RepoName { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil) return } @@ -1011,7 +1011,7 @@ func handleSettingsPostVisibility(ctx *context.Context) { // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin { - ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) return } @@ -1039,21 +1039,21 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R addrErr := err.(*git.ErrInvalidCloneAddr) switch { case addrErr.IsProtocolInvalid: - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form) case addrErr.IsURLError: - ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form) case addrErr.IsPermissionDenied: if addrErr.LocalPath { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form) } else { - ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form) } case addrErr.IsInvalidPath: - ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form) default: ctx.ServerError("Unknown error", err) } return } - ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form) } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 5f775efb220..33f4f7b77bb 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -668,7 +668,7 @@ func NewWikiPost(ctx *context.Context) { } if util.IsEmptyString(form.Title) { - ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form) return } @@ -681,10 +681,10 @@ func NewWikiPost(ctx *context.Context) { if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil { if repo_model.IsErrWikiReservedName(err) { ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form) } else if repo_model.IsErrWikiAlreadyExist(err) { ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form) } else { ctx.ServerError("AddWikiPage", err) } diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 2a6c1f00bcd..b333f364627 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -186,11 +186,11 @@ func EmailPost(ctx *context.Context) { if user_model.IsErrEmailAlreadyUsed(err) { loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) } else { ctx.ServerError("AddEmailAddresses", err) } @@ -251,19 +251,19 @@ func DeleteAccount(ctx *context.Context) { case user_model.IsErrUserNotExist(err): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.user_not_exist"), tplSettingsAccount, nil) case errors.Is(err, smtp.ErrUnsupportedLoginType): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.unsupported_login_type"), tplSettingsAccount, nil) case errors.As(err, &db.ErrUserPasswordNotSet{}): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.unset_password"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.unset_password"), tplSettingsAccount, nil) case errors.As(err, &db.ErrUserPasswordInvalid{}): loadAccountData(ctx) - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) + ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) default: ctx.ServerError("UserSignIn", err) } diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 999bb766837..b78a0ec4347 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -77,7 +77,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_principal_been_used"), tplSettingsKeys, &form) default: ctx.ServerError("AddPrincipalKey", err) } @@ -108,7 +108,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) case asymkey_model.IsErrGPGInvalidTokenSignature(err): loadKeysData(ctx) ctx.Data["Err_Content"] = true @@ -116,7 +116,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) case asymkey_model.IsErrGPGNoEmailFound(err): loadKeysData(ctx) @@ -125,7 +125,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGNoEmailFound).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) default: ctx.ServerError("AddPublicKey", err) } @@ -159,7 +159,7 @@ func KeysPost(ctx *context.Context) { keyID := err.(asymkey_model.ErrGPGInvalidTokenSignature).ID ctx.Data["KeyID"] = keyID ctx.Data["PaddedKeyID"] = asymkey_model.PaddedKeyID(keyID) - ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) default: ctx.ServerError("VerifyGPG", err) } @@ -194,12 +194,12 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) case asymkey_model.IsErrKeyNameAlreadyUsed(err): loadKeysData(ctx) ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) case asymkey_model.IsErrKeyUnableVerify(err): ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") @@ -230,7 +230,7 @@ func KeysPost(ctx *context.Context) { loadKeysData(ctx) ctx.Data["Err_Signature"] = true ctx.Data["Fingerprint"] = err.(asymkey_model.ErrSSHInvalidTokenSignature).Fingerprint - ctx.RenderWithErr(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form) default: ctx.ServerError("VerifySSH", err) } diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go index 78db7650fe7..f71bd518e39 100644 --- a/routers/web/user/setting/security/openid.go +++ b/routers/web/user/setting/security/openid.go @@ -45,7 +45,7 @@ func OpenIDPost(ctx *context.Context) { if err != nil { loadSecurityData(ctx) - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form) return } form.Openid = id @@ -63,7 +63,7 @@ func OpenIDPost(ctx *context.Context) { if obj.URI == id { loadSecurityData(ctx) - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) return } } @@ -73,7 +73,7 @@ func OpenIDPost(ctx *context.Context) { if err != nil { loadSecurityData(ctx) - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form) return } ctx.Redirect(url) @@ -87,7 +87,7 @@ func settingsOpenIDVerify(ctx *context.Context) { id, err := openid.Verify(fullURL) if err != nil { - ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{ + ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{ Openid: id, }) return @@ -98,7 +98,7 @@ func settingsOpenIDVerify(ctx *context.Context) { oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id} if err = user_model.AddUserOpenID(ctx, oid); err != nil { if user_model.IsErrOpenIDAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id}) + ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id}) return } ctx.ServerError("AddUserOpenID", err) diff --git a/services/context/captcha.go b/services/context/captcha.go index b4c3a92907d..79278180b76 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -98,6 +98,6 @@ func VerifyCaptcha(ctx *Context, tpl templates.TplName, form any) { if !valid { ctx.Data["Err_Captcha"] = true - ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form) + ctx.RenderWithErrDeprecated(ctx.Tr("form.captcha_incorrect"), tpl, form) } } diff --git a/services/context/context_response.go b/services/context/context_response.go index bb896024b1a..d057f8d41eb 100644 --- a/services/context/context_response.go +++ b/services/context/context_response.go @@ -124,8 +124,12 @@ func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTM return template.HTML(buf.String()), err } -// RenderWithErr used for page has form validation but need to prompt error to users. -func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) { +// RenderWithErrDeprecated render the page with form validation when it needs to prompt error to users. +// Deprecated: use "form-fetch-action" and JSON response instead. +// WARNING: in many cases, this function is not able to render the page or recover the form fields correctly. +// And it is very difficult to test the page rendered by this function. +// DO NOT USE IT ANYMORE. +func (ctx *Context) RenderWithErrDeprecated(msg any, tpl templates.TplName, form any) { if form != nil { middleware.AssignForm(form, ctx.Data) } From fde7f7db285ed4940ff9bb46b5960602797ff7b0 Mon Sep 17 00:00:00 2001 From: James Robinson Date: Fri, 27 Feb 2026 14:10:01 +0000 Subject: [PATCH 10/50] feat: add branch_count to repository API (#35351) (#36743) Description This PR adds a branch_count field to the repository API response. Currently, clients have to fetch all branches via /branches just to determine the total number of branches. This addition brings Gitea closer to parity with GitLab's API and improves efficiency for UI/CLI clients that need this metric. Linked Issue Fixes #35351 Changes API Structs: Added BranchCount field to Repository struct in modules/structs/repo.go. Database Logic: Implemented CountBranches in models/git/branch.go using XORM for efficient counting. Service Layer: Updated the ToRepo conversion logic in services/convert/repository.go to populate the new field during API serialisation. Tests: Added a new unit test TestCountBranches in models/git/branch_test.go to verify counts (including handling of deleted branches). Screenshots Screenshot 2026-02-24 at 21 41 07 Testing Manually verified the output using curl against a local Gitea instance. Verified that adding a branch increments the count and deleting a branch (soft-delete) decrements it. Ran backend linting: make lint-backend (Passed). Ran specific unit test: go test -v -tags "sqlite sqlite_unlock_notify" ./models/git -run TestCountBranches (Passed). Co-authored-by: silverwind --- models/git/branch.go | 9 +++++++++ models/git/branch_test.go | 22 ++++++++++++++++++++++ modules/structs/repo.go | 1 + services/convert/repository.go | 7 +++++++ templates/swagger/v1_json.tmpl | 5 +++++ 5 files changed, 44 insertions(+) diff --git a/models/git/branch.go b/models/git/branch.go index bc6e2a17486..30d517a06da 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -583,3 +583,12 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o return newBranches, nil } + +// CountBranches returns the number of branches in the repository +func CountBranches(ctx context.Context, repoID int64, includeDeleted bool) (int64, error) { + sess := db.GetEngine(ctx).Where("repo_id=?", repoID) + if !includeDeleted { + sess.And("is_deleted=?", false) + } + return sess.Count(new(Branch)) +} diff --git a/models/git/branch_test.go b/models/git/branch_test.go index 9e6148946d7..b9b761fd5f1 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -263,3 +263,25 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, deletedBranch) } + +func TestCountBranches(t *testing.T) { + // 1. Setup - Exactly like TestAddDeletedBranch + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + // 2. Execution - Using t.Context() to match the rest of the file + initialCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + + // 3. Database Action - Using t.Context() + err = db.Insert(t.Context(), &git_model.Branch{ + RepoID: repo.ID, + Name: "test-branch-for-counting", + }) + assert.NoError(t, err) + + // 4. Verification + newCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + assert.Equal(t, initialCount+1, newCount) +} diff --git a/modules/structs/repo.go b/modules/structs/repo.go index a08cf360371..3507cc410a1 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -73,6 +73,7 @@ type Repository struct { Stars int `json:"stars_count"` Forks int `json:"forks_count"` Watchers int `json:"watchers_count"` + BranchCount int `json:"branch_count"` OpenIssues int `json:"open_issues_count"` OpenPulls int `json:"open_pr_counter"` Releases int `json:"release_counter"` diff --git a/services/convert/repository.go b/services/convert/repository.go index 150c952b15d..658d31d55cd 100644 --- a/services/convert/repository.go +++ b/services/convert/repository.go @@ -8,6 +8,7 @@ import ( "time" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -144,6 +145,11 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR RepoID: repo.ID, }) + branchCount, err := git_model.CountBranches(ctx, repo.ID, false) + if err != nil { + log.Error("CountBranches [%d]: %v", repo.ID, err) + } + mirrorInterval := "" var mirrorUpdated time.Time if repo.IsMirror { @@ -205,6 +211,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR Stars: repo.NumStars, Forks: repo.NumForks, Watchers: repo.NumWatches, + BranchCount: int(branchCount), OpenIssues: repo.NumOpenIssues, OpenPulls: repo.NumOpenPulls, Releases: int(numReleases), diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7b86cc3d45b..212046f8e66 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -28128,6 +28128,11 @@ "type": "string", "x-go-name": "AvatarURL" }, + "branch_count": { + "type": "integer", + "format": "int64", + "x-go-name": "BranchCount" + }, "clone_url": { "type": "string", "x-go-name": "CloneURL" From ae2b19849d2742088f5cdd9a88e1bf6a4b89dd32 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Feb 2026 00:39:26 +0800 Subject: [PATCH 11/50] Use "Enable Gravatar" but not "Disable" (#36771) * Fix #35685 * Fix #35627 * Fix #31112 Introduce "fipped" config value type, remove unused setting variables. Make DisableGravatar=true by defult, remove useless config options from the "Install" page. The legacy config options are still kept because they are still the fallback values for the system config options. --------- Signed-off-by: wxiaoguang --- custom/conf/app.example.ini | 7 ++--- models/user/avatar.go | 2 +- modules/setting/config.go | 2 +- modules/setting/config/value.go | 2 +- modules/setting/picture.go | 25 +++--------------- modules/setting/server.go | 2 -- options/locale/locale_en-US.json | 9 +------ routers/install/install.go | 15 ----------- routers/web/admin/config.go | 1 - services/forms/user_form.go | 3 --- templates/admin/config.tmpl | 2 -- templates/admin/config_settings/avatars.tmpl | 12 ++++++--- templates/install.tmpl | 18 ------------- tests/mssql.ini.tmpl | 5 ---- tests/mysql.ini.tmpl | 5 ---- tests/pgsql.ini.tmpl | 5 ---- tests/sqlite.ini.tmpl | 5 ---- web_src/js/features/admin/config.test.ts | 7 +++++ web_src/js/features/admin/config.ts | 27 ++++++++++++++------ web_src/js/features/install.ts | 19 -------------- 20 files changed, 43 insertions(+), 130 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f5e8f03ae23..adf78ad0f88 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -238,9 +238,6 @@ RUN_USER = ; git ;; Indicate whether to check minimum key size with corresponding type ;MINIMUM_KEY_SIZE_CHECK = false ;; -;; Disable CDN even in "prod" mode -;OFFLINE_MODE = true -;; ;; TLS Settings: Either ACME or manual ;; (Other common TLS configuration are found before) ;ENABLE_ACME = false @@ -1983,12 +1980,12 @@ LEVEL = Info ;; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ;GRAVATAR_SOURCE = gravatar ;; -;; This value will always be true in offline mode. +;; Deprecated, see Web UI Admin Panel -> Config -> Settings ;DISABLE_GRAVATAR = false ;; ;; Federated avatar lookup uses DNS to discover avatar associated ;; with emails, see https://www.libravatar.org -;; This value will always be false in offline mode or when Gravatar is disabled. +;; Deprecated, see Web UI Admin Panel -> Config -> Settings ;ENABLE_FEDERATED_AVATAR = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/user/avatar.go b/models/user/avatar.go index 542bd93b982..8a5ff30bdf5 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -74,7 +74,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { switch { case u.UseCustomAvatar: useLocalAvatar = true - case disableGravatar, setting.OfflineMode: + case disableGravatar: useLocalAvatar = true autoGenerateAvatar = true } diff --git a/modules/setting/config.go b/modules/setting/config.go index bde8e4ac2ab..a41734a843c 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -71,7 +71,7 @@ func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ Picture: &PictureStruct{ - DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), + DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithDefaultSimple(true).WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), }, Repository: &RepositoryStruct{ diff --git a/modules/setting/config/value.go b/modules/setting/config/value.go index bd91add97a9..0ab04ea6568 100644 --- a/modules/setting/config/value.go +++ b/modules/setting/config/value.go @@ -102,7 +102,7 @@ func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool var valStr *string if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue { valStr = &dynVal - } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); has { + } else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue { valStr = &cfgVal } if valStr == nil { diff --git a/modules/setting/picture.go b/modules/setting/picture.go index fafae45baba..d20a110b6c4 100644 --- a/modules/setting/picture.go +++ b/modules/setting/picture.go @@ -22,9 +22,7 @@ var ( RenderedSizeFactor: 2, } - GravatarSource string - DisableGravatar bool // Depreciated: migrated to database - EnableFederatedAvatar bool // Depreciated: migrated to database + GravatarSource string RepoAvatar = struct { Storage *Storage @@ -65,29 +63,12 @@ func loadAvatarsFrom(rootCfg ConfigProvider) error { GravatarSource = source } - DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar()) - deprecatedSettingDB(rootCfg, "", "DISABLE_GRAVATAR") - EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) - deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR") + deprecatedSettingDB(rootCfg, "picture", "DISABLE_GRAVATAR") + deprecatedSettingDB(rootCfg, "picture", "ENABLE_FEDERATED_AVATAR") return nil } -func GetDefaultDisableGravatar() bool { - return OfflineMode -} - -func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { - v := !InstallLock - if OfflineMode { - v = false - } - if disableGravatar { - v = false - } - return v -} - func loadRepoAvatarFrom(rootCfg ConfigProvider) error { sec := rootCfg.Section("picture") diff --git a/modules/setting/server.go b/modules/setting/server.go index a865e942a62..2ea52bc9a55 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -91,7 +91,6 @@ var ( RedirectOtherPort bool RedirectorUseProxyProtocol bool PortToRedirect string - OfflineMode bool CertFile string KeyFile string StaticRootPath string @@ -346,7 +345,6 @@ func loadServerFrom(rootCfg ConfigProvider) { RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol) - OfflineMode = sec.Key("OFFLINE_MODE").MustBool(true) if len(StaticRootPath) == 0 { StaticRootPath = AppWorkPath } diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index bcd28f2deba..a3dc09bb21d 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -285,12 +285,6 @@ "install.register_confirm": "Require Email Confirmation to Register", "install.mail_notify": "Enable Email Notifications", "install.server_service_title": "Server and Third-Party Service Settings", - "install.offline_mode": "Enable Local Mode", - "install.offline_mode_popup": "Disable third-party content delivery networks and serve all resources locally.", - "install.disable_gravatar": "Disable Gravatar", - "install.disable_gravatar_popup": "Disable Gravatar and third-party avatar sources. A default avatar will be used unless a user locally uploads an avatar.", - "install.federated_avatar_lookup": "Enable Federated Avatars", - "install.federated_avatar_lookup_popup": "Enable federated avatar lookup using Libravatar.", "install.disable_registration": "Disable Self-Registration", "install.disable_registration_popup": "Disable user self-registration. Only administrators will be able to create new user accounts.", "install.allow_only_external_registration_popup": "Allow Registration Only Through External Services", @@ -3193,7 +3187,6 @@ "admin.config.custom_conf": "Configuration File Path", "admin.config.custom_file_root_path": "Custom File Root Path", "admin.config.domain": "Server Domain", - "admin.config.offline_mode": "Local Mode", "admin.config.disable_router_log": "Disable Router Log", "admin.config.run_user": "Run As Username", "admin.config.run_mode": "Run Mode", @@ -3296,7 +3289,7 @@ "admin.config.cookie_life_time": "Cookie Life Time", "admin.config.picture_config": "Picture and Avatar Configuration", "admin.config.picture_service": "Picture Service", - "admin.config.disable_gravatar": "Disable Gravatar", + "admin.config.enable_gravatar": "Enable Gravatar", "admin.config.enable_federated_avatar": "Enable Federated Avatars", "admin.config.open_with_editor_app_help": "The \"Open with\" editors for the clone menu. If left empty, the default will be used. Expand to see the default.", "admin.config.git_guide_remote_name": "Repository remote name for git commands in the guide", diff --git a/routers/install/install.go b/routers/install/install.go index 1a60fee3397..81fcdfa384c 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -17,7 +17,6 @@ import ( "code.gitea.io/gitea/models/db" db_install "code.gitea.io/gitea/models/db/install" - system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/generate" @@ -114,11 +113,6 @@ func Install(ctx *context.Context) { form.RegisterConfirm = setting.Service.RegisterEmailConfirm form.MailNotify = setting.Service.EnableNotifyMail - // Server and other services settings - form.OfflineMode = setting.OfflineMode - form.DisableGravatar = setting.DisableGravatar // when installing, there is no database connection so that given a default value - form.EnableFederatedAvatar = setting.EnableFederatedAvatar // when installing, there is no database connection so that given a default value - form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp form.DisableRegistration = setting.Service.DisableRegistration @@ -405,15 +399,6 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(form.RegisterConfirm)) cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify)) - cfg.Section("server").Key("OFFLINE_MODE").SetValue(strconv.FormatBool(form.OfflineMode)) - if err := system_model.SetSettings(ctx, map[string]string{ - setting.Config().Picture.DisableGravatar.DynKey(): strconv.FormatBool(form.DisableGravatar), - setting.Config().Picture.EnableFederatedAvatar.DynKey(): strconv.FormatBool(form.EnableFederatedAvatar), - }); err != nil { - ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form) - return - } - cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn)) cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp)) cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration)) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 79e969fd5e4..a449796ec12 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -126,7 +126,6 @@ func Config(ctx *context.Context) { ctx.Data["AppUrl"] = setting.AppURL ctx.Data["AppBuiltWith"] = setting.AppBuiltWith ctx.Data["Domain"] = setting.Domain - ctx.Data["OfflineMode"] = setting.OfflineMode ctx.Data["RunUser"] = setting.RunUser ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo() diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 618294d4341..cc514a2e279 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -45,9 +45,6 @@ type InstallForm struct { RegisterConfirm bool MailNotify bool - OfflineMode bool - DisableGravatar bool - EnableFederatedAvatar bool EnableOpenIDSignIn bool EnableOpenIDSignUp bool DisableRegistration bool diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 728746713c0..a61dec96203 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -15,8 +15,6 @@
    {{.AppUrl}}
    {{ctx.Locale.Tr "admin.config.domain"}}
    {{.Domain}}
    -
    {{ctx.Locale.Tr "admin.config.offline_mode"}}
    -
    {{svg (Iif .OfflineMode "octicon-check" "octicon-x")}}
    {{ctx.Locale.Tr "admin.config.disable_router_log"}}
    {{svg (Iif .DisableRouterLog "octicon-check" "octicon-x")}}
    diff --git a/templates/admin/config_settings/avatars.tmpl b/templates/admin/config_settings/avatars.tmpl index 1fc761034d4..7a2e40a68ee 100644 --- a/templates/admin/config_settings/avatars.tmpl +++ b/templates/admin/config_settings/avatars.tmpl @@ -3,17 +3,21 @@
    -
    {{ctx.Locale.Tr "admin.config.disable_gravatar"}}
    + {{$cfgOpt := .SystemConfig.Picture.DisableGravatar}} +
    {{ctx.Locale.Tr "admin.config.enable_gravatar"}}
    -
    - +
    +
    +
    + + {{$cfgOpt = .SystemConfig.Picture.EnableFederatedAvatar}}
    {{ctx.Locale.Tr "admin.config.enable_federated_avatar"}}
    - +
    diff --git a/templates/install.tmpl b/templates/install.tmpl index acfafd3cc70..45f14d5c575 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -203,24 +203,6 @@ {{ctx.Locale.Tr "install.server_service_title"}} -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl index 0d169050334..6f031691f01 100644 --- a/tests/mssql.ini.tmpl +++ b/tests/mssql.ini.tmpl @@ -41,7 +41,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2201 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w BUILTIN_SSH_SERVER_USER = git SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= @@ -62,10 +61,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index bf59efde4cc..02069b403e6 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -43,7 +43,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2201 BUILTIN_SSH_SERVER_USER = git START_SSH_SERVER = true -OFFLINE_MODE = false LFS_START_SERVER = true LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w @@ -65,10 +64,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index b6fcd33f700..50a649fc791 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -42,7 +42,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2202 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w BUILTIN_SSH_SERVER_USER = git SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= @@ -63,10 +62,6 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org ENABLE_NOTIFY_MAIL = true -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 243bea86f13..b43b658bb08 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -37,7 +37,6 @@ SSH_LISTEN_HOST = localhost SSH_PORT = 2203 START_SSH_SERVER = true LFS_START_SERVER = true -OFFLINE_MODE = false LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w ENABLE_GZIP = true BUILTIN_SSH_SERVER_USER = git @@ -59,10 +58,6 @@ DEFAULT_KEEP_EMAIL_PRIVATE = false DEFAULT_ALLOW_CREATE_ORGANIZATION = true NO_REPLY_ADDRESS = noreply.example.org -[picture] -DISABLE_GRAVATAR = false -ENABLE_FEDERATED_AVATAR = false - [session] PROVIDER = file diff --git a/web_src/js/features/admin/config.test.ts b/web_src/js/features/admin/config.test.ts index e44ccb2a940..3de524070cb 100644 --- a/web_src/js/features/admin/config.test.ts +++ b/web_src/js/features/admin/config.test.ts @@ -7,10 +7,15 @@ test('ConfigFormValueMapper', () => { + + + + + @@ -35,6 +40,8 @@ test('ConfigFormValueMapper', () => { expect(result).toEqual({ 'k1': 'true', 'k2': '"k2-val"', + 'k-flipped-false': 'false', + 'k-flipped-true': 'true', 'repository.open-with.editor-apps': '[{"DisplayName":"a","OpenURL":"b"}]', // TODO: OPEN-WITH-EDITOR-APP-JSON: it must match backend 'struct': '{"SubBoolean":true,"SubTimestamp":123456780,"OtherKey":"other-value","NewKey":"new-value"}', }); diff --git a/web_src/js/features/admin/config.ts b/web_src/js/features/admin/config.ts index 047c1a46a44..5df81412f9b 100644 --- a/web_src/js/features/admin/config.ts +++ b/web_src/js/features/admin/config.ts @@ -6,14 +6,23 @@ import {submitFormFetchAction} from '../common-fetch-action.ts'; const {appSubUrl} = window.config; +function collectCheckboxBooleanValue(el: HTMLInputElement): boolean { + const valType = el.getAttribute('data-config-value-type') as ConfigValueType; + if (valType === 'boolean') return el.checked; + if (valType === 'flipped') return !el.checked; + requireExplicitValueType(el); +} + function initSystemConfigAutoCheckbox(el: HTMLInputElement) { el.addEventListener('change', async () => { // if the checkbox is inside a form, we assume it's handled by the form submit and do not send an individual request if (el.closest('form')) return; try { - const resp = await POST(`${appSubUrl}/-/admin/config`, { - data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key')!, value: String(el.checked)}), + const data = new URLSearchParams({ + key: el.getAttribute('data-config-dyn-key')!, + value: String(collectCheckboxBooleanValue(el)), }); + const resp = await POST(`${appSubUrl}/-/admin/config`, {data}); const json: Record = await resp.json(); if (json.errorMessage) throw new Error(json.errorMessage); } catch (ex) { @@ -47,7 +56,7 @@ function extractElemConfigSubKey(el: GeneralFormFieldElement, dynKey: string): s // Due to the different design between HTML form elements and the JSON struct of the config values, we need to explicitly define some types. // * checkbox can be used for boolean value, it can also be used for multiple values (array) -type ConfigValueType = 'boolean' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. +type ConfigValueType = 'boolean' | 'flipped' | 'string' | 'number' | 'timestamp'; // TODO: support more types like array, not used at the moment. function toDatetimeLocalValue(unixSeconds: number) { const d = new Date(unixSeconds * 1000); @@ -102,13 +111,14 @@ export class ConfigFormValueMapper { return true; } - collectConfigValueFromElement(el: GeneralFormFieldElement, _oldVal: any = null) { + collectConfigValueFromElement(el: GeneralFormFieldElement) { let val: any; const valType = this.presetValueTypes[el.name]; if (el.matches('[type="checkbox"]')) { - if (valType !== 'boolean') requireExplicitValueType(el); - val = el.checked; - // oldVal: for future use when we support array value with checkbox + // TODO: if it needs to support array values in the future, + // it needs to iterate the "namedElems" to find all the checkboxes with the same name and collect values accordingly, + // and set the namedElems[matchedIdx] to null to avoid duplicate processing. + val = collectCheckboxBooleanValue(el); } else if (el.matches('[type="datetime-local"]')) { if (valType !== 'timestamp') requireExplicitValueType(el); val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null. @@ -128,7 +138,7 @@ export class ConfigFormValueMapper { if (!el) continue; const subKey = extractElemConfigSubKey(el, dynKey); if (!subKey) continue; // if not match, skip - cfgVal[subKey] = this.collectConfigValueFromElement(el, cfgVal[subKey]); + cfgVal[subKey] = this.collectConfigValueFromElement(el); namedElems[idx] = null; } } @@ -180,6 +190,7 @@ export class ConfigFormValueMapper { // now, the namedElems should only contain the config options without sub values, // directly store the value in formData with key as the element name, for example: + // "foo.enabled" => "true" for (const el of namedElems) { if (!el) continue; const dynKey = el.name; diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index c00fe3c03d4..f316cd532dc 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -60,25 +60,6 @@ function initPreInstall() { } // TODO: better handling of exclusive relations. - document.querySelector('#offline-mode input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#disable-gravatar input')!.checked = true; - document.querySelector('#federated-avatar-lookup input')!.checked = false; - } - }); - document.querySelector('#disable-gravatar input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#federated-avatar-lookup input')!.checked = false; - } else { - document.querySelector('#offline-mode input')!.checked = false; - } - }); - document.querySelector('#federated-avatar-lookup input')!.addEventListener('change', function () { - if (this.checked) { - document.querySelector('#disable-gravatar input')!.checked = false; - document.querySelector('#offline-mode input')!.checked = false; - } - }); document.querySelector('#enable-openid-signin input')!.addEventListener('change', function () { if (this.checked) { if (!document.querySelector('#disable-registration input')!.checked) { From 50ec48d9fee605f9ffa9210ae8eaddb9530ea840 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Feb 2026 17:45:10 +0100 Subject: [PATCH 12/50] Move Fomantic dropdown CSS to custom module (#36530) Moved fomantic dropdown css to custom module, tested on the dropdown devtest page, it renders exactly the same as before while using roughly 50% less CSS. The clean up was very conservative, likely more can be done in the future. Also, this fixes a bug present on main branch where dropdown border has incorrect color on hover. --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.5 --- stylelint.config.js | 2 +- web_src/css/base.css | 237 +-- web_src/css/index.css | 1 + web_src/css/modules/divider.css | 5 - web_src/css/modules/dropdown.css | 958 +++++++++ .../fomantic/build/components/dropdown.css | 1755 ----------------- web_src/fomantic/build/components/dropdown.js | 2 +- web_src/fomantic/build/fomantic.css | 1 - web_src/fomantic/semantic.json | 1 - 9 files changed, 962 insertions(+), 2000 deletions(-) create mode 100644 web_src/css/modules/dropdown.css delete mode 100644 web_src/fomantic/build/components/dropdown.css diff --git a/stylelint.config.js b/stylelint.config.js index 42edf76f434..3e6be3c2487 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -125,7 +125,7 @@ export default { 'csstools/value-no-unknown-custom-properties': [true, {importFrom: cssVarFiles}], 'declaration-block-no-duplicate-properties': [true, {ignore: ['consecutive-duplicates-with-different-values']}], 'declaration-block-no-redundant-longhand-properties': [true, {ignoreShorthands: ['flex-flow', 'overflow', 'grid-template']}], - 'declaration-property-unit-disallowed-list': {'line-height': ['em']}, + 'declaration-property-unit-disallowed-list': null, 'declaration-property-value-disallowed-list': {'word-break': ['break-word']}, 'font-family-name-quotes': 'always-where-recommended', 'function-name-case': 'lower', diff --git a/web_src/css/base.css b/web_src/css/base.css index 3fa5c1246cd..59072839a9b 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -243,9 +243,7 @@ progress::-moz-progress-bar { color: var(--color-white); } -::placeholder, -.ui.dropdown:not(.button) > .default.text, -.ui.default.dropdown:not(.button) > .text { +::placeholder { color: var(--color-placeholder-text) !important; opacity: 1 !important; } @@ -397,83 +395,6 @@ a.label, color: var(--color-text-light-2); } -.ui.dropdown .menu { - background: var(--color-menu); - border-color: var(--color-secondary); -} - -.ui.dropdown .menu > .header { - text-transform: none; /* reset fomantic's "uppercase" */ -} - -.ui.dropdown .menu > .header:not(.ui) { - color: var(--color-text); - font-size: 0.95em; /* reset fomantic's small font-size */ -} - -.ui.dropdown .menu > .item { - color: var(--color-text); - line-height: var(--line-height-default); -} - -.ui.dropdown .menu > .item:hover { - color: var(--color-text); - background: var(--color-hover); -} - -.ui.dropdown .menu > .item:active { - color: var(--color-text); - background: var(--color-active); -} - -.ui.dropdown .menu .active.item { - color: var(--color-text); - background: var(--color-active); - border-radius: 0; - font-weight: var(--font-weight-normal); -} - -/* fix misaligned images in webhook dropdown */ -.ui.dropdown .menu > .item > img { - margin-top: -0.25rem; - margin-bottom: -0.25rem; -} -.ui.dropdown .menu > .item > svg { - margin-right: .78rem; /* use the same margin as for */ -} - -.ui.selection.dropdown .menu > .item { - border-color: var(--color-secondary); -} -.ui.selection.dropdown .menu .item:first-of-type { - border-radius: 0; -} -.ui.selection.visible.dropdown > .text:not(.default) { - color: var(--color-text); -} - -.ui.dropdown.selected, -.ui.dropdown .menu .selected.item { - color: var(--color-text); - background: var(--color-hover); -} - -.ui.dropdown .menu > .message:not(.ui) { - color: var(--color-text-light-2); -} - -/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */ -.ui.dropdown > .text > .img { - margin-left: 0; - float: none; - margin-right: 0.78571429rem; -} - -.ui.dropdown > .text > .description, -.ui.dropdown .menu > .item > .description { - color: var(--color-text-light-2); -} - /* styles from removed fomantic transition module */ .hidden.transition { visibility: hidden; @@ -484,23 +405,6 @@ a.label, visibility: visible !important; } -.ui.selection.active.dropdown, -.ui.selection.active.dropdown:hover, -.ui.selection.active.dropdown .menu, -.ui.selection.active.dropdown:hover .menu { - border-color: var(--color-primary); -} - -.ui.pointing.dropdown > .menu:not(.hidden)::after { - background: var(--color-menu); - box-shadow: -1px -1px 0 0 var(--color-secondary); -} - -.ui.pointing.upward.dropdown .menu::after, -.ui.top.pointing.upward.dropdown .menu::after { - box-shadow: 1px 1px 0 0 var(--color-secondary); -} - .ui.comments .comment .metadata { color: var(--color-text-light-2); } @@ -553,20 +457,6 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -/* popover box shadows */ -.ui.dropdown .menu, -.ui.upward.dropdown > .menu, -.ui.menu .dropdown.item .menu, -.ui.selection.active.dropdown .menu, -.ui.upward.selection.dropdown .menu, -.ui.selection.active.dropdown:hover .menu, -.ui.upward.active.selection.dropdown:hover .menu { - box-shadow: 0 6px 18px var(--color-shadow); -} -.ui.floating.dropdown .menu { - box-shadow: 0 6px 18px var(--color-shadow) !important; -} - .ui .message.flash-message { text-align: center; } @@ -618,27 +508,11 @@ img.ui.avatar, border: 1px solid; } -.ui.dropdown .menu.context-user-switch .scrolling.menu { - border-radius: 0 !important; - box-shadow: none !important; - border-bottom: 1px solid var(--color-secondary); - max-width: 80vw; -} - .user-menu > .item { width: 100%; border-radius: 0 !important; } -.scrolling.menu .item.selected { - font-weight: var(--font-weight-semibold) !important; -} - -.ui.dropdown .scrolling.menu { - border-color: var(--color-secondary); - border-radius: 0 0 var(--border-radius) var(--border-radius) !important; -} - .color-preview { display: inline-block; margin-left: 0.4em; @@ -914,22 +788,6 @@ table th[data-sortt-desc] .svg { margin-left: 0.25rem; } -.ui.dropdown .menu .item { - border-radius: 0; -} - -.ui.dropdown .menu .item:first-of-type { - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.ui.dropdown .menu .item:last-of-type { - border-radius: 0 0 var(--border-radius) var(--border-radius); -} - -.ui.multiple.dropdown > .label { - box-shadow: 0 0 0 1px var(--color-secondary) inset; -} - /* for "image" emojis like ":git:" ":gitea:" and ":github:" (see CUSTOM_EMOJIS config option) */ .emoji img { border-width: 0 !important; @@ -978,54 +836,6 @@ table th[data-sortt-desc] .svg { min-height: 0; } -.ui.dropdown:not(.button) { - line-height: var(--line-height-default); /* the dropdown doesn't have default line-height, use this to make the dropdown icon align with plain dropdown */ -} - -/* dropdown has some kinds of icons: -- "> .dropdown.icon": the arrow for opening the dropdown -- "> .remove.icon": the "x" icon for clearing the dropdown, only used in selection dropdown -- "> .ui.label > .delete.icon": the "x" icon for removing a label item in multiple selection dropdown -*/ - -.ui.dropdown.mini.button, -.ui.dropdown.tiny.button { - padding-right: 20px; -} -.ui.dropdown.button { - padding-right: 22px; -} -.ui.dropdown.large.button { - padding-right: 24px; -} - -/* Gitea uses SVG images instead of Fomantic builtin "" font icons, so we need to reset the icon styles */ -.ui.ui.dropdown > .icon.icon { - position: initial; /* plain dropdown and button dropdown use flex layout for icons */ - padding: 0; - margin: 0; - height: auto; -} - -.ui.ui.dropdown > .icon.icon:hover { - opacity: 1; -} - -.ui.ui.button.dropdown > .icon.icon, -.ui.ui.selection.dropdown > .icon.icon { - position: absolute; /* selection dropdown uses absolute layout for icons */ - top: 50%; - transform: translateY(-50%); -} - -.ui.ui.dropdown > .dropdown.icon { - right: 0.5em; -} - -.ui.ui.dropdown > .remove.icon { - right: 2em; -} - .btn, .ui.ui.dropdown, .flex-text-inline, @@ -1038,18 +848,6 @@ table th[data-sortt-desc] .svg { min-width: 0; /* make ellipsis work */ } -.ui.multiple.selection.dropdown { - flex-wrap: wrap; -} - -.ui.ui.dropdown.selection { - min-width: 14em; /* match the default min width */ -} - -.ui.dropdown .ui.label .svg { - vertical-align: middle; -} - .ui.ui.labeled.button { gap: 0; align-items: stretch; @@ -1066,44 +864,11 @@ table th[data-sortt-desc] .svg { min-width: 0; } -.ui.dropdown > .ui.button, .flex-text-block > .ui.button, .flex-text-inline > .ui.button { margin: 0; /* fomantic buttons have default margin, when we use them in a flex container with gap, we do not need these margins */ } -/* 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: var(--gap-block); - min-width: 0; -} -.ui.dropdown .menu.flex-items-menu > .item img, -.ui.dropdown .menu.flex-items-menu > .item svg { - margin: 0; /* use gap, but not margin */ -} - -.ui.dropdown.ellipsis-text-items { - /* reset y padding and use the line-height below instead, to avoid the "overflow: hidden" clips the larger image in the "text" element */ - padding-top: 0; - padding-bottom: 0; -} - -.ui.dropdown.ellipsis-text-items > .text { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - line-height: 2.71; /* matches fomantic dropdown's default min-height */ -} - -.ui.dropdown.ellipsis-text-items .menu > .item { - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; -} - .svg.octicon-file-directory-fill, .svg.octicon-file-directory-open-fill, .svg.octicon-file-submodule { diff --git a/web_src/css/index.css b/web_src/css/index.css index 699ba221ca5..3edb3df6c1c 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -20,6 +20,7 @@ @import "./modules/modal.css"; @import "./modules/tab.css"; @import "./modules/form.css"; +@import "./modules/dropdown.css"; @import "./modules/shortcut.css"; @import "./modules/tippy.css"; diff --git a/web_src/css/modules/divider.css b/web_src/css/modules/divider.css index acc8408f376..a60b7d52cbe 100644 --- a/web_src/css/modules/divider.css +++ b/web_src/css/modules/divider.css @@ -36,8 +36,3 @@ h4.divider { .divider.divider-text::after { margin-left: .75em; } - -.ui.dropdown .menu > .divider { - border-top: 1px solid var(--color-secondary); - margin: 4px 0; -} diff --git a/web_src/css/modules/dropdown.css b/web_src/css/modules/dropdown.css new file mode 100644 index 00000000000..2008ece2ed9 --- /dev/null +++ b/web_src/css/modules/dropdown.css @@ -0,0 +1,958 @@ +/* These are the remnants of the fomantic dropdown module */ + +.ui.dropdown { + cursor: pointer; + position: relative; + display: inline-block; + outline: none; + text-align: left; + user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.ui.dropdown .menu { + cursor: auto; + position: absolute; + display: none; + outline: none; + top: 100%; + min-width: max-content; + margin: 0; + padding: 0; + background: var(--color-menu); + font-size: 1em; + text-align: left; + box-shadow: 0 6px 18px var(--color-shadow); + border: 1px solid var(--color-secondary); + border-radius: 0.28571429rem; + z-index: 11; + left: 0; +} + +.ui.dropdown .menu > * { + white-space: nowrap; +} + +.ui.dropdown > input:not(.search):first-child, +.ui.dropdown > select { + display: none !important; +} + +.ui.dropdown > .dropdown.icon { + line-height: 1; + height: 1em; + width: auto; + backface-visibility: hidden; + text-align: center; +} + +.ui.dropdown:not(.labeled) > .dropdown.icon { + position: relative; + width: auto; + font-size: 0.85714286em; + margin: 0 0 0 1em; +} + +.ui.dropdown > .text { + display: inline-block; +} + +.ui.dropdown .menu > .item { + position: relative; + cursor: pointer; + display: block; + border: none; + height: auto; + min-height: 2.57142857rem; + text-align: left; + border-top: none; + line-height: var(--line-height-default); + font-size: 1rem; + color: var(--color-text); + padding: 0.78571429rem 1.14285714rem !important; + text-transform: none; + font-weight: var(--font-weight-normal); + box-shadow: none; + -webkit-touch-callout: none; +} + +.ui.dropdown .menu > .item:first-child { + border-top-width: 0; +} + +.ui.dropdown .menu > .header { + margin: 1rem 0 0.75rem; + padding: 0 1.14285714rem; + font-weight: var(--font-weight-medium); + text-transform: none; +} + +.ui.dropdown .menu > .header:not(.ui) { + color: var(--color-text); + font-size: 0.95em; +} + +.ui.dropdown .menu > .divider { + border-top: 1px solid var(--color-secondary); + height: 0; + margin: 4px 0; +} + +.ui.dropdown.dropdown .menu > .input { + width: auto; + display: flex; + margin: 1.14285714rem 0.78571429rem; + min-width: 10rem; +} + +.ui.dropdown .menu > .header + .input { + margin-top: 0; +} + +.ui.dropdown .menu > .input:not(.transparent) input { + padding: 0.5em 1em; +} + +.ui.dropdown .menu > .input:not(.transparent) .button, +.ui.dropdown .menu > .input:not(.transparent) i.icon, +.ui.dropdown .menu > .input:not(.transparent) .label { + padding-top: 0.5em; + padding-bottom: 0.5em; +} + +.ui.dropdown > .text > .description, +.ui.dropdown .menu > .item > .description { + float: right; + margin: 0 0 0 1em; + color: var(--color-text-light-2); +} + +.ui.dropdown .menu > .message { + padding: 0.78571429rem 1.14285714rem; + font-weight: var(--font-weight-normal); +} + +.ui.dropdown .menu > .message:not(.ui) { + color: var(--color-text-light-2); +} + +/* Remove Menu Item Divider */ +.ui.dropdown .ui.menu > .item::before, +.ui.menu .ui.dropdown .menu > .item::before { + display: none; +} + +/* Prevent Menu Item Border */ +.ui.menu .ui.dropdown .menu .active.item { + border-left: none; +} + +/* Automatically float dropdown menu right on last menu item */ +.ui.menu .right.menu .dropdown:last-child > .menu:not(.left), +.ui.menu .right.dropdown.item > .menu:not(.left), +.ui.buttons > .ui.dropdown:last-child > .menu:not(.left) { + left: auto; + right: 0; +} + +.ui.button.dropdown .menu { + min-width: 100%; +} + +select.ui.dropdown { + height: 38px; + padding: 0.5em; + border: 1px solid var(--color-input-border); + visibility: visible; +} + +.ui.selection.dropdown { + cursor: pointer; + overflow-wrap: break-word; + line-height: 1em; + white-space: normal; + outline: 0; + transform: rotateZ(0deg); + min-width: 14em; + min-height: 2.71428571em; + background: var(--color-input-background); + display: inline-block; + padding: 0.78571429em 3.2em 0.78571429em 1em; + color: var(--color-input-text); + box-shadow: none; + border: 1px solid var(--color-input-border); + border-radius: 0.28571429rem; +} + +.ui.selection.dropdown.visible, +.ui.selection.dropdown.active { + z-index: 10; +} + +.ui.selection.dropdown > .search.icon, +.ui.selection.dropdown > .delete.icon, +.ui.selection.dropdown > .dropdown.icon { + cursor: pointer; + position: absolute; + width: auto; + height: auto; + line-height: 1.21428571em; + top: 0.78571429em; + right: 1em; + z-index: 3; + margin: -0.78571429em; + padding: 0.91666667em; + opacity: 0.8; +} + +.ui.selection.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; + border-top-width: 0 !important; + outline: none; + margin: 0 -1px; + min-width: calc(100% + 2px); + width: calc(100% + 2px); + border-radius: 0 0 0.28571429rem 0.28571429rem; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.dropdown .menu::after, +.ui.selection.dropdown .menu::before { + display: none; +} + +.ui.selection.dropdown .menu > .message { + padding: 0.78571429rem 1.14285714rem; +} + +@media only screen and (max-width: 767.98px) { + .ui.selection.dropdown .menu { + max-height: 8.01428571rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.selection.dropdown .menu { + max-height: 10.68571429rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.selection.dropdown .menu { + max-height: 16.02857143rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.selection.dropdown .menu { + max-height: 21.37142857rem; + } +} + +.ui.selection.dropdown .menu > .item { + border-top: 1px solid var(--color-secondary); + padding: 0.78571429rem 1.14285714rem !important; + white-space: normal; + overflow-wrap: normal; +} + +.ui.selection.dropdown .menu .item:first-of-type { + border-radius: 0; +} + +.ui.selection.dropdown .menu > .hidden.addition.item { + display: none; +} + +.ui.selection.dropdown:hover { + border-color: var(--color-input-border-hover); + box-shadow: none; +} + +.ui.selection.active.dropdown { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.active.dropdown .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.dropdown:focus { + border-color: var(--color-primary); + box-shadow: none; +} + +.ui.selection.dropdown:focus .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.visible.dropdown > .text:not(.default) { + font-weight: var(--font-weight-normal); + color: var(--color-text); +} + +.ui.selection.active.dropdown:hover { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.selection.active.dropdown:hover .menu { + border-color: var(--color-primary); + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.active.selection.dropdown > .dropdown.icon, +.ui.visible.selection.dropdown > .dropdown.icon { + z-index: 3; +} + +.ui.active.selection.dropdown { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.ui.active.empty.selection.dropdown { + border-radius: 0.28571429rem !important; + box-shadow: none !important; +} + +.ui.active.empty.selection.dropdown .menu { + border: none !important; + box-shadow: none !important; +} + +.ui.search.dropdown > input.search { + background: none transparent !important; + border: none !important; + box-shadow: none !important; + cursor: text; + top: 0; + left: 1px; + width: 100%; + outline: none; + -webkit-tap-highlight-color: transparent; + padding: inherit; + position: absolute; + z-index: 2; +} + +.ui.search.dropdown > .text { + cursor: text; + position: relative; + left: 1px; + z-index: auto; +} + +.ui.search.selection.dropdown > input.search { + line-height: 1.21428571em; + padding: 0.67857143em 3.2em 0.67857143em 1em; +} + +.ui.search.selection.dropdown > span.sizer { + line-height: 1.21428571em; + padding: 0.67857143em 3.2em 0.67857143em 1em; + display: none; + white-space: pre; +} + +.ui.search.dropdown.active > input.search, +.ui.search.dropdown.visible > input.search { + cursor: auto; +} + +.ui.search.dropdown.active > .text, +.ui.search.dropdown.visible > .text { + pointer-events: none; +} + +.ui.active.search.dropdown input.search:focus + .text i.icon { + opacity: var(--opacity-disabled); +} + +.ui.active.search.dropdown input.search:focus + .text { + color: var(--color-placeholder-text) !important; +} + +.ui.search.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; +} + +@media only screen and (max-width: 767.98px) { + .ui.search.dropdown .menu { + max-height: 8.01428571rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.search.dropdown .menu { + max-height: 10.68571429rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.search.dropdown .menu { + max-height: 16.02857143rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.search.dropdown .menu { + max-height: 21.37142857rem; + } +} + +.ui.dropdown > .remove.icon { + cursor: pointer; + font-size: 0.85714286em; + margin: -0.78571429em; + padding: 0.91666667em; + right: 3em; + top: 0.78571429em; + position: absolute; + opacity: 0.6; + z-index: 3; +} + +.ui.clearable.dropdown .text, +.ui.clearable.dropdown a:last-of-type { + margin-right: 1.5em; +} + +.ui.dropdown select.noselection ~ .remove.icon, +.ui.dropdown input[value=""] ~ .remove.icon, +.ui.dropdown input:not([value]) ~ .remove.icon, +.ui.dropdown.loading > .remove.icon { + display: none; +} + +.ui.ui.multiple.dropdown { + padding: 0.22619048em 3.2em 0.22619048em 0.35714286em; +} + +.ui.multiple.dropdown .menu { + cursor: auto; +} + +.ui.multiple.dropdown > .label { + display: inline-block; + white-space: normal; + font-size: 1em; + padding: 0.35714286em 0.78571429em; + margin: 0.14285714rem 0.28571429rem 0.14285714rem 0; + box-shadow: 0 0 0 1px var(--color-secondary) inset; +} + +/* Text */ +.ui.multiple.dropdown > .text { + position: static; + padding: 0; + max-width: 100%; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + line-height: 1.21428571em; +} + +.ui.multiple.dropdown > .text.default { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ui.multiple.dropdown > .label ~ input.search { + margin-left: 0.14285714em !important; +} + +.ui.multiple.dropdown > .label ~ .text { + display: none; +} + +.ui.multiple.search.dropdown, +.ui.multiple.search.dropdown > input.search { + cursor: text; +} + +.ui.multiple.search.dropdown > .text { + display: inline-block; + position: absolute; + top: 0; + left: 0; + padding: inherit; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + line-height: 1.21428571em; +} + +.ui.multiple.search.dropdown > .label ~ .text { + display: none; +} + +.ui.multiple.search.dropdown > input.search { + position: static; + padding: 0; + max-width: 100%; + margin: 0.45238095em 0 0.45238095em 0.64285714em; + width: 2.2em; + line-height: 1.21428571em; +} + +.ui.dropdown .menu .active.item { + background: var(--color-active); + font-weight: var(--font-weight-normal); + color: var(--color-text); + box-shadow: none; + z-index: 12; + border-radius: 0; +} + +.ui.dropdown .menu > .item:hover { + background: var(--color-hover); + color: var(--color-text); + z-index: 13; +} + +.ui.dropdown .menu > .item:active { + color: var(--color-text); + background: var(--color-active); +} + +.ui.dropdown:not(.button) > .default.text, +.ui.default.dropdown:not(.button) > .text { + color: var(--color-placeholder-text); +} + +.ui.dropdown:not(.button) > input:focus ~ .default.text, +.ui.default.dropdown:not(.button) > input:focus ~ .text { + color: var(--color-placeholder-text); +} + +.ui.loading.dropdown > i.icon { + height: 1em !important; +} + +.ui.loading.selection.dropdown > i.icon { + padding: 1.5em 1.28571429em !important; +} + +.ui.loading.dropdown > i.icon::before { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + border-radius: 500rem; + border: 0.2em solid var(--color-secondary); +} + +.ui.loading.dropdown > i.icon::after { + position: absolute; + content: ""; + top: 50%; + left: 50%; + box-shadow: 0 0 0 1px transparent; + margin: -0.64285714em 0 0 -0.64285714em; + width: 1.28571429em; + height: 1.28571429em; + animation: loader 0.6s infinite linear; + border: 0.2em solid var(--color-text-light-2); + border-radius: 500rem; +} + +.ui.dropdown .loading.menu { + display: block; + visibility: hidden; + z-index: -1; +} + +.ui.dropdown > .loading.menu { + left: 0 !important; + right: auto !important; +} + +.ui.dropdown > .menu .loading.menu { + left: 100% !important; + right: auto !important; +} + +.ui.dropdown.selected, +.ui.dropdown .menu .selected.item { + color: var(--color-text); + background: var(--color-hover); +} + +.ui.dropdown > .filtered.text { + visibility: hidden; +} + +.ui.dropdown .filtered.item { + display: none !important; +} + +.ui.disabled.dropdown, +.ui.dropdown .menu > .disabled.item { + cursor: default; + pointer-events: none; + opacity: var(--opacity-disabled); +} + +.ui.dropdown > .left.menu { + left: auto !important; + right: 0 !important; +} + +.ui.upward.dropdown > .menu { + top: auto; + bottom: 100%; + box-shadow: 0 6px 18px var(--color-shadow); + border-radius: 0.28571429rem 0.28571429rem 0 0; +} + +.ui.dropdown .upward.menu { + top: auto !important; + bottom: 0 !important; +} + +.ui.upward.selection.dropdown .menu { + border-top-width: 1px !important; + border-bottom-width: 0 !important; + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.upward.selection.dropdown:hover { + box-shadow: 0 0 2px 0 var(--color-shadow); +} + +.ui.active.upward.selection.dropdown { + border-radius: 0 0 0.28571429rem 0.28571429rem !important; +} + +.ui.upward.selection.dropdown.visible { + box-shadow: 0 0 3px 0 var(--color-shadow); + border-radius: 0 0 0.28571429rem 0.28571429rem !important; +} + +.ui.upward.active.selection.dropdown:hover { + box-shadow: 0 0 3px 0 var(--color-shadow); +} + +.ui.upward.active.selection.dropdown:hover .menu { + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.scrolling.dropdown .menu, +.ui.dropdown .scrolling.menu { + overflow-x: hidden; + overflow-y: auto; +} + +.ui.scrolling.dropdown .menu { + overflow-x: hidden; + overflow-y: auto; + backface-visibility: hidden; + -webkit-overflow-scrolling: touch; + min-width: 100% !important; + width: auto !important; +} + +.ui.dropdown .scrolling.menu { + position: static; + overflow-y: auto; + border: none; + box-shadow: none !important; + margin: 0 !important; + min-width: 100% !important; + width: auto !important; + border-top: 1px solid var(--color-secondary); + border-color: var(--color-secondary); + border-radius: 0 0 var(--border-radius) var(--border-radius) !important; +} + +.ui.scrolling.dropdown .menu .item.item.item, +.ui.dropdown .scrolling.menu > .item.item.item { + border-top: none; +} + +.ui.scrolling.dropdown .menu .item:first-child, +.ui.dropdown .scrolling.menu .item:first-child { + border-top: none; +} + +.ui.dropdown > .animating.menu .scrolling.menu, +.ui.dropdown > .visible.menu .scrolling.menu { + display: block; +} + +@media only screen and (max-width: 767.98px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 10.28571429rem; + } +} + +@media only screen and (min-width: 768px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 15.42857143rem; + } +} + +@media only screen and (min-width: 992px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 20.57142857rem; + } +} + +@media only screen and (min-width: 1920px) { + .ui.scrolling.dropdown .menu, + .ui.dropdown .scrolling.menu { + max-height: 20.57142857rem; + } +} + +.ui.fluid.dropdown { + display: block; + width: 100% !important; + min-width: 0; +} + +.ui.fluid.dropdown > .dropdown.icon { + float: right; +} + +.ui.floating.dropdown .menu { + left: 0; + right: auto; + box-shadow: 0 6px 18px var(--color-shadow) !important; + border-radius: 0.28571429rem !important; +} + +.ui.floating.dropdown > .menu { + border-radius: 0.28571429rem !important; +} + +.ui:not(.upward).floating.dropdown > .menu { + margin-top: 0.5em; +} + +.ui.upward.floating.dropdown > .menu { + margin-bottom: 0.5em; +} + +.ui.pointing.dropdown > .menu { + top: 100%; + margin-top: 0.78571429rem; + border-radius: 0.28571429rem; +} + +.ui.pointing.dropdown > .menu:not(.hidden)::after { + display: block; + position: absolute; + pointer-events: none; + content: ""; + visibility: visible; + transform: rotate(45deg); + width: 0.5em; + height: 0.5em; + box-shadow: -1px -1px 0 0 var(--color-secondary); + background: var(--color-menu); + z-index: 2; + top: -0.25em; + left: 50%; + margin: 0 0 0 -0.25em; +} + +.ui.top.right.pointing.dropdown > .menu { + inset: 100% 0 auto auto; + margin: 1em 0 0; +} + +.ui.top.pointing.dropdown > .left.menu::after, +.ui.top.right.pointing.dropdown > .menu::after { + top: -0.25em; + left: auto !important; + right: 1em !important; + margin: 0; + transform: rotate(45deg); +} + +.ui.dropdown, +.ui.dropdown .menu > .item { + font-size: 1rem; +} + +.ui.mini.dropdown, +.ui.mini.dropdown .menu > .item { + font-size: 0.78571429rem; +} + +.ui.tiny.dropdown, +.ui.tiny.dropdown .menu > .item { + font-size: 0.85714286rem; +} + +.ui.small.dropdown, +.ui.small.dropdown .menu > .item { + font-size: 0.92857143rem; +} + +/* This rule must come AFTER .ui.selection.dropdown because both have + specificity (0,3,0) and source order determines the winner. + In the original codebase this was in base.css which loaded after fomantic. */ +.ui.dropdown:not(.button) { + line-height: var(--line-height-default); +} + +/* Icons / Flags / Labels / Image */ +.ui.dropdown > .text > img, +.ui.dropdown > .text > .image, +.ui.dropdown .menu > .item > .image, +.ui.dropdown .menu > .item > img { + margin-left: 0; + float: none; + margin-right: 0.78571429rem; +} + +.ui.dropdown .menu > .item > svg { + margin-right: 0.78rem; +} + +/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */ +.ui.dropdown > .text > .img { + margin-left: 0; + float: none; + margin-right: 0.78571429rem; +} + +.ui.dropdown > .text > img, +.ui.dropdown > .text > .image:not(.icon), +.ui.dropdown .menu > .item > .image:not(.icon), +.ui.dropdown .menu > .item > img { + display: inline-block; + vertical-align: top; + width: auto; + margin-top: -0.25rem; + margin-bottom: -0.25rem; + max-height: 2em; +} + +.ui.dropdown .menu .item { + border-radius: 0; +} + +.ui.dropdown .menu .item:first-of-type { + border-radius: var(--border-radius) var(--border-radius) 0 0; +} + +.ui.dropdown .menu .item:last-of-type { + border-radius: 0 0 var(--border-radius) var(--border-radius); +} + +/* Gitea uses SVG images instead of Fomantic builtin "" font icons, so we need to reset the icon styles */ +.ui.ui.dropdown > .icon.icon { + position: initial; + padding: 0; + margin: 0; + height: auto; +} + +.ui.ui.dropdown > .icon.icon:hover { + opacity: 1; +} + +.ui.ui.button.dropdown > .icon.icon, +.ui.ui.selection.dropdown > .icon.icon { + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.ui.ui.dropdown > .dropdown.icon { + right: 0.5em; +} + +.ui.ui.dropdown > .remove.icon { + right: 2em; +} + +.ui.dropdown.mini.button, +.ui.dropdown.tiny.button { + padding-right: 20px; +} + +.ui.dropdown.button { + padding-right: 22px; +} + +.ui.multiple.selection.dropdown { + flex-wrap: wrap; +} + +.ui.ui.dropdown.selection { + min-width: 14em; +} + +.ui.dropdown .ui.label .svg { + vertical-align: middle; +} + +.ui.dropdown > .ui.button { + margin: 0; +} + +/* popover box shadow for menu dropdown */ +.ui.menu .dropdown.item .menu { + box-shadow: 0 6px 18px var(--color-shadow); +} + +.ui.dropdown .menu.context-user-switch .scrolling.menu { + border-radius: 0 !important; + box-shadow: none !important; + border-bottom: 1px solid var(--color-secondary); + max-width: 80vw; +} + +.scrolling.menu .item.selected { + font-weight: var(--font-weight-semibold) !important; +} + +/* 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: var(--gap-block); + min-width: 0; +} + +.ui.dropdown .menu.flex-items-menu > .item img, +.ui.dropdown .menu.flex-items-menu > .item svg { + margin: 0; +} + +.ui.dropdown.ellipsis-text-items { + /* reset y padding and use the line-height below instead, to avoid the "overflow: hidden" clips the larger image in the "text" element */ + padding-top: 0; + padding-bottom: 0; +} + +.ui.dropdown.ellipsis-text-items > .text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + line-height: 2.71; +} + +.ui.dropdown.ellipsis-text-items .menu > .item { + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} diff --git a/web_src/fomantic/build/components/dropdown.css b/web_src/fomantic/build/components/dropdown.css deleted file mode 100644 index b7b35a2f05c..00000000000 --- a/web_src/fomantic/build/components/dropdown.css +++ /dev/null @@ -1,1755 +0,0 @@ -/*! - * # Fomantic-UI - Dropdown - * http://github.com/fomantic/Fomantic-UI/ - * - * - * Released under the MIT license - * http://opensource.org/licenses/MIT - * - */ - - -/******************************* - Dropdown -*******************************/ - -.ui.dropdown { - cursor: pointer; - position: relative; - display: inline-block; - outline: none; - text-align: left; - transition: box-shadow 0.1s ease, width 0.1s ease; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - - -/******************************* - Content -*******************************/ - - -/*-------------- - Menu ----------------*/ - -.ui.dropdown .menu { - cursor: auto; - position: absolute; - display: none; - outline: none; - top: 100%; - min-width: -moz-max-content; - min-width: max-content; - margin: 0; - padding: 0 0; - background: #FFFFFF; - font-size: 1em; - text-shadow: none; - text-align: left; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); - border: 1px solid rgba(34, 36, 38, 0.15); - border-radius: 0.28571429rem; - transition: opacity 0.1s ease; - z-index: 11; - will-change: transform, opacity; -} -.ui.dropdown .menu > * { - white-space: nowrap; -} - -/*-------------- - Hidden Input ----------------*/ - -.ui.dropdown > input:not(.search):first-child, -.ui.dropdown > select { - display: none !important; -} - -/*-------------- - Dropdown Icon ----------------*/ - -.ui.dropdown:not(.labeled) > .dropdown.icon { - position: relative; - width: auto; - font-size: 0.85714286em; - margin: 0 0 0 1em; -} -.ui.dropdown .menu > .item .dropdown.icon { - width: auto; - float: right; - margin: 0em 0 0 1em; -} -.ui.dropdown .menu > .item .dropdown.icon + .text { - margin-right: 1em; -} - -/*-------------- - Text ----------------*/ - -.ui.dropdown > .text { - display: inline-block; - transition: none; -} - -/*-------------- - Menu Item ----------------*/ - -.ui.dropdown .menu > .item { - position: relative; - cursor: pointer; - display: block; - border: none; - height: auto; - min-height: 2.57142857rem; - text-align: left; - border-top: none; - line-height: 1em; - font-size: 1rem; - color: rgba(0, 0, 0, 0.87); - padding: 0.78571429rem 1.14285714rem !important; - text-transform: none; - font-weight: normal; - box-shadow: none; - -webkit-touch-callout: none; -} -.ui.dropdown .menu > .item:first-child { - border-top-width: 0; -} -.ui.dropdown .menu > .item.vertical { - display: flex; - flex-direction: column-reverse; -} - -/*-------------- - Floated Content ----------------*/ - -.ui.dropdown > .text > [class*="right floated"], -.ui.dropdown .menu .item > [class*="right floated"] { - float: right !important; - margin-right: 0 !important; - margin-left: 1em !important; -} -.ui.dropdown > .text > [class*="left floated"], -.ui.dropdown .menu .item > [class*="left floated"] { - float: left !important; - margin-left: 0 !important; - margin-right: 1em !important; -} -.ui.dropdown .menu .item > i.icon.floated, -.ui.dropdown .menu .item > .flag.floated, -.ui.dropdown .menu .item > .image.floated, -.ui.dropdown .menu .item > img.floated { - margin-top: 0em; -} - -/*-------------- - Menu Divider ----------------*/ - -.ui.dropdown .menu > .header { - margin: 1rem 0 0.75rem; - padding: 0 1.14285714rem; - font-weight: 500; - text-transform: uppercase; -} -.ui.dropdown .menu > .header:not(.ui) { - color: rgba(0, 0, 0, 0.85); - font-size: 0.78571429em; -} -.ui.dropdown .menu > .divider { - border-top: 1px solid rgba(34, 36, 38, 0.1); - height: 0; - margin: 0.5em 0; -} -.ui.dropdown .menu > .horizontal.divider { - border-top: none; -} -.ui.dropdown.dropdown .menu > .input { - width: auto; - display: flex; - margin: 1.14285714rem 0.78571429rem; - min-width: 10rem; -} -.ui.dropdown .menu > .header + .input { - margin-top: 0; -} -.ui.dropdown .menu > .input:not(.transparent) input { - padding: 0.5em 1em; -} -.ui.dropdown .menu > .input:not(.transparent) .button, -.ui.dropdown .menu > .input:not(.transparent) i.icon, -.ui.dropdown .menu > .input:not(.transparent) .label { - padding-top: 0.5em; - padding-bottom: 0.5em; -} - -/*----------------- - Item Description --------------------*/ - -.ui.dropdown > .text > .description, -.ui.dropdown .menu > .item > .description { - float: right; - margin: 0 0 0 1em; - color: rgba(0, 0, 0, 0.4); -} -.ui.dropdown .menu > .item.vertical > .description { - margin: 0; -} - -/*----------------- - Item Text --------------------*/ - -.ui.dropdown .menu > .item.vertical > .text { - margin-bottom: 0.25em; -} - -/*----------------- - Message --------------------*/ - -.ui.dropdown .menu > .message { - padding: 0.78571429rem 1.14285714rem; - font-weight: normal; -} -.ui.dropdown .menu > .message:not(.ui) { - color: rgba(0, 0, 0, 0.4); -} - -/*-------------- - Sub Menu ----------------*/ - -.ui.dropdown .menu .menu { - top: 0; - left: 100%; - right: auto; - margin: 0 -0.5em !important; - border-radius: 0.28571429rem !important; - z-index: 21 !important; -} - -/* Hide Arrow */ -.ui.dropdown .menu .menu:after { - display: none; -} - -/*-------------- - Sub Elements ----------------*/ - - -/* Icons / Flags / Labels / Image */ -.ui.dropdown > .text > i.icon, -.ui.dropdown > .text > .label, -.ui.dropdown > .text > .flag, -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image { - margin-top: 0em; -} -.ui.dropdown .menu > .item > i.icon, -.ui.dropdown .menu > .item > .label, -.ui.dropdown .menu > .item > .flag, -.ui.dropdown .menu > .item > .image, -.ui.dropdown .menu > .item > img { - margin-top: 0em; -} -.ui.dropdown > .text > i.icon, -.ui.dropdown > .text > .label, -.ui.dropdown > .text > .flag, -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image, -.ui.dropdown .menu > .item > i.icon, -.ui.dropdown .menu > .item > .label, -.ui.dropdown .menu > .item > .flag, -.ui.dropdown .menu > .item > .image, -.ui.dropdown .menu > .item > img { - margin-left: 0; - float: none; - margin-right: 0.78571429rem; -} - -/*-------------- - Image ----------------*/ - -.ui.dropdown > .text > img, -.ui.dropdown > .text > .image:not(.icon), -.ui.dropdown .menu > .item > .image:not(.icon), -.ui.dropdown .menu > .item > img { - display: inline-block; - vertical-align: top; - width: auto; - margin-top: -0.5em; - margin-bottom: -0.5em; - max-height: 2em; -} - - -/******************************* - Coupling -*******************************/ - - -/*-------------- - Menu ----------------*/ - - -/* Remove Menu Item Divider */ -.ui.dropdown .ui.menu > .item:before, -.ui.menu .ui.dropdown .menu > .item:before { - display: none; -} - -/* Prevent Menu Item Border */ -.ui.menu .ui.dropdown .menu .active.item { - border-left: none; -} - -/* Automatically float dropdown menu right on last menu item */ -.ui.menu .right.menu .dropdown:last-child > .menu:not(.left), -.ui.menu .right.dropdown.item > .menu:not(.left), -.ui.buttons > .ui.dropdown:last-child > .menu:not(.left) { - left: auto; - right: 0; -} - -/*-------------- - Label - ---------------*/ - - -/* Dropdown Menu */ -.ui.label.dropdown .menu { - min-width: 100%; -} - -/*-------------- - Button - ---------------*/ - - -/* No Margin On Icon Button */ -.ui.dropdown.icon.button > .dropdown.icon { - margin: 0; -} -.ui.button.dropdown .menu { - min-width: 100%; -} - - -/******************************* - Types -*******************************/ - -select.ui.dropdown { - height: 38px; - padding: 0.5em; - border: 1px solid rgba(34, 36, 38, 0.15); - visibility: visible; -} - -/*-------------- - Selection - ---------------*/ - - -/* Displays like a select box */ -.ui.selection.dropdown { - cursor: pointer; - word-wrap: break-word; - line-height: 1em; - white-space: normal; - outline: 0; - transform: rotateZ(0deg); - min-width: 14em; - min-height: 2.71428571em; - background: #FFFFFF; - display: inline-block; - padding: 0.78571429em 3.2em 0.78571429em 1em; - color: rgba(0, 0, 0, 0.87); - box-shadow: none; - border: 1px solid rgba(34, 36, 38, 0.15); - border-radius: 0.28571429rem; - transition: box-shadow 0.1s ease, width 0.1s ease; -} -.ui.selection.dropdown.visible, -.ui.selection.dropdown.active { - z-index: 10; -} -.ui.selection.dropdown > .search.icon, -.ui.selection.dropdown > .delete.icon, -.ui.selection.dropdown > .dropdown.icon { - cursor: pointer; - position: absolute; - width: auto; - height: auto; - line-height: 1.21428571em; - top: 0.78571429em; - right: 1em; - z-index: 3; - margin: -0.78571429em; - padding: 0.91666667em; - opacity: 0.8; - transition: opacity 0.1s ease; -} - -/* Compact */ -.ui.compact.selection.dropdown { - min-width: 0; -} - -/* Selection Menu */ -.ui.selection.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; - border-top-width: 0 !important; - width: auto; - outline: none; - margin: 0 -1px; - min-width: calc(100% + 2px); - width: calc(100% + 2px); - border-radius: 0 0 0.28571429rem 0.28571429rem; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); - transition: opacity 0.1s ease; -} -.ui.selection.dropdown .menu:after, -.ui.selection.dropdown .menu:before { - display: none; -} - -/*-------------- - Message - ---------------*/ - -.ui.selection.dropdown .menu > .message { - padding: 0.78571429rem 1.14285714rem; -} -@media only screen and (max-width: 767.98px) { - .ui.selection.dropdown.short .menu { - max-height: 6.01071429rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 4.00714286rem; - } - .ui.selection.dropdown .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown.long .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 24.04285714rem; - } -} -@media only screen and (min-width: 768px) { - .ui.selection.dropdown.short .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 5.34285714rem; - } - .ui.selection.dropdown .menu { - max-height: 10.68571429rem; - } - .ui.selection.dropdown.long .menu { - max-height: 21.37142857rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 32.05714286rem; - } -} -@media only screen and (min-width: 992px) { - .ui.selection.dropdown.short .menu { - max-height: 12.02142857rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 8.01428571rem; - } - .ui.selection.dropdown .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown.long .menu { - max-height: 32.05714286rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 48.08571429rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.selection.dropdown.short .menu { - max-height: 16.02857143rem; - } - .ui.selection.dropdown[class*="very short"] .menu { - max-height: 10.68571429rem; - } - .ui.selection.dropdown .menu { - max-height: 21.37142857rem; - } - .ui.selection.dropdown.long .menu { - max-height: 42.74285714rem; - } - .ui.selection.dropdown[class*="very long"] .menu { - max-height: 64.11428571rem; - } -} - -/* Menu Item */ -.ui.selection.dropdown .menu > .item { - border-top: 1px solid #FAFAFA; - padding: 0.78571429rem 1.14285714rem !important; - white-space: normal; - word-wrap: normal; -} - -/* User Item */ -.ui.selection.dropdown .menu > .hidden.addition.item { - display: none; -} - -/* Hover */ -.ui.selection.dropdown:hover { - border-color: rgba(34, 36, 38, 0.35); - box-shadow: none; -} - -/* Active */ -.ui.selection.active.dropdown { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} -.ui.selection.active.dropdown .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Focus */ -.ui.selection.dropdown:focus { - border-color: #96C8DA; - box-shadow: none; -} -.ui.selection.dropdown:focus .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Visible */ -.ui.selection.visible.dropdown > .text:not(.default) { - font-weight: normal; - color: rgba(0, 0, 0, 0.8); -} - -/* Visible Hover */ -.ui.selection.active.dropdown:hover { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} -.ui.selection.active.dropdown:hover .menu { - border-color: #96C8DA; - box-shadow: 0 2px 3px 0 rgba(34, 36, 38, 0.15); -} - -/* Dropdown Icon */ -.ui.active.selection.dropdown > .dropdown.icon, -.ui.visible.selection.dropdown > .dropdown.icon { - opacity: ''; - z-index: 3; -} - -/* Connecting Border */ -.ui.active.selection.dropdown { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -/* Empty Connecting Border */ -.ui.active.empty.selection.dropdown { - border-radius: 0.28571429rem !important; - box-shadow: none !important; -} -.ui.active.empty.selection.dropdown .menu { - border: none !important; - box-shadow: none !important; -} - -/* CSS specific to iOS devices or firefox mobile only */ -@supports (-webkit-touch-callout: none) or (-webkit-overflow-scrolling: touch) or (-moz-appearance:none) { - @media (-moz-touch-enabled), (pointer: coarse) { - .ui.dropdown .scrollhint.menu:not(.hidden):before { - animation: scrollhint 2s ease 2; - content: ''; - z-index: 15; - display: block; - position: absolute; - opacity: 0; - right: 0.25em; - top: 0; - height: 100%; - border-right: 0.25em solid; - border-left: 0; - -o-border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%; - border-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0)) 1 100%; - } - .ui.inverted.dropdown .scrollhint.menu:not(.hidden):before { - -o-border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%; - border-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0)) 1 100%; - } - @keyframes scrollhint { - 0% { - opacity: 1; - top: 100%; - } - 100% { - opacity: 0; - top: 0; - } - } - } -} - -/*-------------- - Searchable - ---------------*/ - - -/* Search Selection */ -.ui.search.dropdown { - min-width: ''; -} - -/* Search Dropdown */ -.ui.search.dropdown > input.search { - background: none transparent !important; - border: none !important; - box-shadow: none !important; - cursor: text; - top: 0; - left: 1px; - width: 100%; - outline: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); - padding: inherit; -} - -/* Text Layering */ -.ui.search.dropdown > input.search { - position: absolute; - z-index: 2; -} -.ui.search.dropdown > .text { - cursor: text; - position: relative; - left: 1px; - z-index: auto; -} - -/* Search Selection */ -.ui.search.selection.dropdown > input.search { - line-height: 1.21428571em; - padding: 0.67857143em 3.2em 0.67857143em 1em; -} - -/* Used to size multi select input to character width */ -.ui.search.selection.dropdown > span.sizer { - line-height: 1.21428571em; - padding: 0.67857143em 3.2em 0.67857143em 1em; - display: none; - white-space: pre; -} - -/* Active/Visible Search */ -.ui.search.dropdown.active > input.search, -.ui.search.dropdown.visible > input.search { - cursor: auto; -} -.ui.search.dropdown.active > .text, -.ui.search.dropdown.visible > .text { - pointer-events: none; -} - -/* Filtered Text */ -.ui.active.search.dropdown input.search:focus + .text i.icon, -.ui.active.search.dropdown input.search:focus + .text .flag { - opacity: var(--opacity-disabled); -} -.ui.active.search.dropdown input.search:focus + .text { - color: rgba(115, 115, 115, 0.87) !important; -} -.ui.search.dropdown.button > span.sizer { - display: none; -} - -/* Search Menu */ -.ui.search.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; -} -@media only screen and (max-width: 767.98px) { - .ui.search.dropdown .menu { - max-height: 8.01428571rem; - } -} -@media only screen and (min-width: 768px) { - .ui.search.dropdown .menu { - max-height: 10.68571429rem; - } -} -@media only screen and (min-width: 992px) { - .ui.search.dropdown .menu { - max-height: 16.02857143rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.search.dropdown .menu { - max-height: 21.37142857rem; - } -} - -/* Clearable Selection */ -.ui.dropdown > .remove.icon { - cursor: pointer; - font-size: 0.85714286em; - margin: -0.78571429em; - padding: 0.91666667em; - right: 3em; - top: 0.78571429em; - position: absolute; - opacity: 0.6; - z-index: 3; -} -.ui.clearable.dropdown .text, -.ui.clearable.dropdown a:last-of-type { - margin-right: 1.5em; -} -.ui.dropdown select.noselection ~ .remove.icon, -.ui.dropdown input[value=''] ~ .remove.icon, -.ui.dropdown input:not([value]) ~ .remove.icon, -.ui.dropdown.loading > .remove.icon { - display: none; -} - -/*-------------- - Multiple - ---------------*/ - - -/* Multiple Selection */ -.ui.ui.multiple.dropdown { - padding: 0.22619048em 3.2em 0.22619048em 0.35714286em; -} -.ui.multiple.dropdown .menu { - cursor: auto; -} - -/* Selection Label */ -.ui.multiple.dropdown > .label { - display: inline-block; - white-space: normal; - font-size: 1em; - padding: 0.35714286em 0.78571429em; - margin: 0.14285714rem 0.28571429rem 0.14285714rem 0; - box-shadow: 0 0 0 1px rgba(34, 36, 38, 0.15) inset; -} - -/* Dropdown Icon */ -.ui.multiple.dropdown .dropdown.icon { - margin: ''; - padding: ''; -} - -/* Text */ -.ui.multiple.dropdown > .text { - position: static; - padding: 0; - max-width: 100%; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - line-height: 1.21428571em; -} -.ui.multiple.dropdown > .text.default { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.ui.multiple.dropdown > .label ~ input.search { - margin-left: 0.14285714em !important; -} -.ui.multiple.dropdown > .label ~ .text { - display: none; -} -.ui.multiple.dropdown > .label:not(.image) > img:not(.centered) { - margin-right: 0.78571429rem; -} -.ui.multiple.dropdown > .label:not(.image) > img.ui:not(.avatar) { - margin-bottom: 0.39285714rem; -} -.ui.multiple.dropdown > .image.label img { - margin: -0.35714286em 0.78571429em -0.35714286em -0.78571429em; - height: 1.71428571em; -} - -/*----------------- - Multiple Search - -----------------*/ - - -/* Multiple Search Selection */ -.ui.multiple.search.dropdown, -.ui.multiple.search.dropdown > input.search { - cursor: text; -} - -/* Prompt Text */ -.ui.multiple.search.dropdown > .text { - display: inline-block; - position: absolute; - top: 0; - left: 0; - padding: inherit; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - line-height: 1.21428571em; -} -.ui.multiple.search.dropdown > .label ~ .text { - display: none; -} - -/* Search */ -.ui.multiple.search.dropdown > input.search { - position: static; - padding: 0; - max-width: 100%; - margin: 0.45238095em 0 0.45238095em 0.64285714em; - width: 2.2em; - line-height: 1.21428571em; -} -.ui.multiple.search.dropdown.button { - min-width: 14em; -} - -/*-------------- - Inline - ---------------*/ - -.ui.inline.dropdown { - cursor: pointer; - display: inline-block; - color: inherit; -} -.ui.inline.dropdown .dropdown.icon { - margin: 0 0.21428571em 0 0.21428571em; - vertical-align: baseline; -} -.ui.inline.dropdown > .text { - font-weight: 500; -} -.ui.inline.dropdown .menu { - cursor: auto; - margin-top: 0.21428571em; - border-radius: 0.28571429rem; -} - - -/******************************* - States -*******************************/ - - -/*-------------------- - Active -----------------------*/ - - -/* Menu Item Active */ -.ui.dropdown .menu .active.item { - background: transparent; - font-weight: 500; - color: rgba(0, 0, 0, 0.95); - box-shadow: none; - z-index: 12; -} - -/*-------------------- - Hover -----------------------*/ - - -/* Menu Item Hover */ -.ui.dropdown .menu > .item:hover { - background: rgba(0, 0, 0, 0.05); - color: rgba(0, 0, 0, 0.95); - z-index: 13; -} - -/*-------------------- - Default Text -----------------------*/ - -.ui.dropdown:not(.button) > .default.text, -.ui.default.dropdown:not(.button) > .text { - color: rgba(191, 191, 191, 0.87); -} -.ui.dropdown:not(.button) > input:focus ~ .default.text, -.ui.default.dropdown:not(.button) > input:focus ~ .text { - color: rgba(115, 115, 115, 0.87); -} - -/*-------------------- - Loading - ---------------------*/ - -.ui.loading.dropdown > i.icon { - height: 1em !important; -} -.ui.loading.selection.dropdown > i.icon { - padding: 1.5em 1.28571429em !important; -} -.ui.loading.dropdown > i.icon:before { - position: absolute; - content: ''; - top: 50%; - left: 50%; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - border-radius: 500rem; - border: 0.2em solid rgba(0, 0, 0, 0.1); -} -.ui.loading.dropdown > i.icon:after { - position: absolute; - content: ''; - top: 50%; - left: 50%; - box-shadow: 0 0 0 1px transparent; - margin: -0.64285714em 0 0 -0.64285714em; - width: 1.28571429em; - height: 1.28571429em; - animation: loader 0.6s infinite linear; - border: 0.2em solid #767676; - border-radius: 500rem; -} - -/* Coupling */ -.ui.loading.dropdown.button > i.icon:before, -.ui.loading.dropdown.button > i.icon:after { - display: none; -} -.ui.loading.dropdown > .text { - transition: none; -} - -/* Used To Check Position */ -.ui.dropdown .loading.menu { - display: block; - visibility: hidden; - z-index: -1; -} -.ui.dropdown > .loading.menu { - left: 0 !important; - right: auto !important; -} -.ui.dropdown > .menu .loading.menu { - left: 100% !important; - right: auto !important; -} - -/*-------------------- - Keyboard Select -----------------------*/ - - -/* Selected Item */ -.ui.dropdown.selected, -.ui.dropdown .menu .selected.item { - background: rgba(0, 0, 0, 0.03); - color: rgba(0, 0, 0, 0.95); -} - -/*-------------------- - Search Filtered -----------------------*/ - - -/* Filtered Item */ -.ui.dropdown > .filtered.text { - visibility: hidden; -} -.ui.dropdown .filtered.item { - display: none !important; -} - -/*-------------------- - States - ----------------------*/ - -.ui.dropdown.error, -.ui.dropdown.error > .text, -.ui.dropdown.error > .default.text { - color: #9F3A38; -} -.ui.selection.dropdown.error { - background: #FFF6F6; - border-color: #E0B4B4; -} -.ui.selection.dropdown.error:hover { - border-color: #E0B4B4; -} -.ui.multiple.selection.error.dropdown > .label { - border-color: #E0B4B4; -} -.ui.dropdown.error > .menu, -.ui.dropdown.error > .menu .menu { - border-color: #E0B4B4; -} -.ui.dropdown.error > .menu > .item { - color: #9F3A38; -} - -/* Item Hover */ -.ui.dropdown.error > .menu > .item:hover { - background-color: #FBE7E7; -} - -/* Item Active */ -.ui.dropdown.error > .menu .active.item { - background-color: #FDCFCF; -} -.ui.dropdown.info, -.ui.dropdown.info > .text, -.ui.dropdown.info > .default.text { - color: #276F86; -} -.ui.selection.dropdown.info { - background: #F8FFFF; - border-color: #A9D5DE; -} -.ui.selection.dropdown.info:hover { - border-color: #A9D5DE; -} -.ui.multiple.selection.info.dropdown > .label { - border-color: #A9D5DE; -} -.ui.dropdown.info > .menu, -.ui.dropdown.info > .menu .menu { - border-color: #A9D5DE; -} -.ui.dropdown.info > .menu > .item { - color: #276F86; -} - -/* Item Hover */ -.ui.dropdown.info > .menu > .item:hover { - background-color: #e9f2fb; -} - -/* Item Active */ -.ui.dropdown.info > .menu .active.item { - background-color: #cef1fd; -} -.ui.dropdown.success, -.ui.dropdown.success > .text, -.ui.dropdown.success > .default.text { - color: #2C662D; -} -.ui.selection.dropdown.success { - background: #FCFFF5; - border-color: #A3C293; -} -.ui.selection.dropdown.success:hover { - border-color: #A3C293; -} -.ui.multiple.selection.success.dropdown > .label { - border-color: #A3C293; -} -.ui.dropdown.success > .menu, -.ui.dropdown.success > .menu .menu { - border-color: #A3C293; -} -.ui.dropdown.success > .menu > .item { - color: #2C662D; -} - -/* Item Hover */ -.ui.dropdown.success > .menu > .item:hover { - background-color: #e9fbe9; -} - -/* Item Active */ -.ui.dropdown.success > .menu .active.item { - background-color: #dafdce; -} -.ui.dropdown.warning, -.ui.dropdown.warning > .text, -.ui.dropdown.warning > .default.text { - color: #573A08; -} -.ui.selection.dropdown.warning { - background: #FFFAF3; - border-color: #C9BA9B; -} -.ui.selection.dropdown.warning:hover { - border-color: #C9BA9B; -} -.ui.multiple.selection.warning.dropdown > .label { - border-color: #C9BA9B; -} -.ui.dropdown.warning > .menu, -.ui.dropdown.warning > .menu .menu { - border-color: #C9BA9B; -} -.ui.dropdown.warning > .menu > .item { - color: #573A08; -} - -/* Item Hover */ -.ui.dropdown.warning > .menu > .item:hover { - background-color: #fbfbe9; -} - -/* Item Active */ -.ui.dropdown.warning > .menu .active.item { - background-color: #fdfdce; -} - -/*-------------------- - Clear -----------------------*/ - -.ui.dropdown > .clear.dropdown.icon { - opacity: 0.8; - transition: opacity 0.1s ease; -} -.ui.dropdown > .clear.dropdown.icon:hover { - opacity: 1; -} - -/*-------------------- - Disabled - ----------------------*/ - - -/* Disabled */ -.ui.disabled.dropdown, -.ui.dropdown .menu > .disabled.item { - cursor: default; - pointer-events: none; - opacity: var(--opacity-disabled); -} - - -/******************************* - Variations -*******************************/ - - -/*-------------- - Direction ----------------*/ - - -/* Flyout Direction */ -.ui.dropdown .menu { - left: 0; -} - -/* Default Side (Right) */ -.ui.dropdown .right.menu > .menu, -.ui.dropdown .menu .right.menu { - left: 100% !important; - right: auto !important; - border-radius: 0.28571429rem !important; -} - -/* Leftward Opening Menu */ -.ui.dropdown > .left.menu { - left: auto !important; - right: 0 !important; -} -.ui.dropdown > .left.menu .menu, -.ui.dropdown .menu .left.menu { - left: auto; - right: 100%; - margin: 0 -0.5em 0 0 !important; - border-radius: 0.28571429rem !important; -} -.ui.dropdown .item .left.dropdown.icon, -.ui.dropdown .left.menu .item .dropdown.icon { - width: auto; - float: left; - margin: 0em 0 0 0; -} -.ui.dropdown .item .left.dropdown.icon, -.ui.dropdown .left.menu .item .dropdown.icon { - width: auto; - float: left; - margin: 0em 0 0 0; -} -.ui.dropdown .item .left.dropdown.icon + .text, -.ui.dropdown .left.menu .item .dropdown.icon + .text { - margin-left: 1em; - margin-right: 0; -} - -/*-------------- - Upward - ---------------*/ - - -/* Upward Main Menu */ -.ui.upward.dropdown > .menu { - top: auto; - bottom: 100%; - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08); - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Upward Sub Menu */ -.ui.dropdown .upward.menu { - top: auto !important; - bottom: 0 !important; -} - -/* Active Upward */ -.ui.simple.upward.active.dropdown, -.ui.simple.upward.dropdown:hover { - border-radius: 0.28571429rem 0.28571429rem 0 0 !important; -} -.ui.upward.dropdown.button:not(.pointing):not(.floating).active { - border-radius: 0.28571429rem 0.28571429rem 0 0; -} - -/* Selection */ -.ui.upward.selection.dropdown .menu { - border-top-width: 1px !important; - border-bottom-width: 0 !important; - box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08); -} -.ui.upward.selection.dropdown:hover { - box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.05); -} - -/* Active Upward */ -.ui.active.upward.selection.dropdown { - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -/* Visible Upward */ -.ui.upward.selection.dropdown.visible { - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.08); - border-radius: 0 0 0.28571429rem 0.28571429rem !important; -} - -/* Visible Hover Upward */ -.ui.upward.active.selection.dropdown:hover { - box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.05); -} -.ui.upward.active.selection.dropdown:hover .menu { - box-shadow: 0 -2px 3px 0 rgba(0, 0, 0, 0.08); -} - -/*-------------- - Scrolling - ---------------*/ - - -/* Selection Menu */ -.ui.scrolling.dropdown .menu, -.ui.dropdown .scrolling.menu { - overflow-x: hidden; - overflow-y: auto; -} -.ui.scrolling.dropdown .menu { - overflow-x: hidden; - overflow-y: auto; - backface-visibility: hidden; - -webkit-overflow-scrolling: touch; - min-width: 100% !important; - width: auto !important; -} -.ui.dropdown .scrolling.menu { - position: static; - overflow-y: auto; - border: none; - box-shadow: none !important; - border-radius: 0 !important; - margin: 0 !important; - min-width: 100% !important; - width: auto !important; - border-top: 1px solid rgba(34, 36, 38, 0.15); -} -.ui.scrolling.dropdown .menu .item.item.item, -.ui.dropdown .scrolling.menu > .item.item.item { - border-top: none; -} -.ui.scrolling.dropdown .menu .item:first-child, -.ui.dropdown .scrolling.menu .item:first-child { - border-top: none; -} -.ui.dropdown > .animating.menu .scrolling.menu, -.ui.dropdown > .visible.menu .scrolling.menu { - display: block; -} - -/* Scrollbar in IE */ -@media all and (-ms-high-contrast: none) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - min-width: calc(100% - 17px); - } -} -@media only screen and (max-width: 767.98px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 10.28571429rem; - } -} -@media only screen and (min-width: 768px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 15.42857143rem; - } -} -@media only screen and (min-width: 992px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 20.57142857rem; - } -} -@media only screen and (min-width: 1920px) { - .ui.scrolling.dropdown .menu, - .ui.dropdown .scrolling.menu { - max-height: 20.57142857rem; - } -} - -/*-------------- - Columnar ----------------*/ - -.ui.column.dropdown > .menu { - flex-wrap: wrap; -} -.ui.dropdown[class*="two column"] > .menu > .item { - width: 50%; -} -.ui.dropdown[class*="three column"] > .menu > .item { - width: 33%; -} -.ui.dropdown[class*="four column"] > .menu > .item { - width: 25%; -} -.ui.dropdown[class*="five column"] > .menu > .item { - width: 20%; -} - -/*-------------- - Simple - ---------------*/ - - -/* Displays without javascript */ -.ui.simple.dropdown .menu:before, -.ui.simple.dropdown .menu:after { - display: none; -} -.ui.simple.dropdown .menu { - position: absolute; - -/* IE hack to make dropdown icons appear inline */ - display: -ms-inline-flexbox !important; - display: block; - overflow: hidden; - top: -9999px; - opacity: 0; - width: 0; - height: 0; - transition: opacity 0.1s ease; - margin-top: 0 !important; -} -.ui.simple.active.dropdown, -.ui.simple.dropdown:hover { - border-bottom-left-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} -.ui.simple.active.dropdown > .menu, -.ui.simple.dropdown:hover > .menu { - overflow: visible; - width: auto; - height: auto; - top: 100%; - opacity: 1; -} -.ui.simple.dropdown > .menu > .item:active > .menu, -.ui.simple.dropdown .menu .item:hover > .menu { - overflow: visible; - width: auto; - height: auto; - top: 0 !important; - left: 100%; - opacity: 1; -} -.ui.simple.dropdown > .menu > .item:active > .left.menu, -.ui.simple.dropdown .menu .item:hover > .left.menu, -.right.menu .ui.simple.dropdown > .menu > .item:active > .menu:not(.right), -.right.menu .ui.simple.dropdown > .menu .item:hover > .menu:not(.right) { - left: auto; - right: 100%; -} -.ui.simple.disabled.dropdown:hover .menu { - display: none; - height: 0; - width: 0; - overflow: hidden; -} - -/* Visible */ -.ui.simple.visible.dropdown > .menu { - display: block; -} - -/* Scrolling */ -.ui.simple.scrolling.active.dropdown > .menu, -.ui.simple.scrolling.dropdown:hover > .menu { - overflow-x: hidden; - overflow-y: auto; -} - -/*-------------- - Fluid - ---------------*/ - -.ui.fluid.dropdown { - display: block; - width: 100% !important; - min-width: 0; -} -.ui.fluid.dropdown > .dropdown.icon { - float: right; -} - -/*-------------- - Floating - ---------------*/ - -.ui.floating.dropdown .menu { - left: 0; - right: auto; - box-shadow: 0 2px 4px 0 rgba(34, 36, 38, 0.12), 0 2px 10px 0 rgba(34, 36, 38, 0.15) !important; - border-radius: 0.28571429rem !important; -} -.ui.floating.dropdown > .menu { - border-radius: 0.28571429rem !important; -} -.ui:not(.upward).floating.dropdown > .menu { - margin-top: 0.5em; -} -.ui.upward.floating.dropdown > .menu { - margin-bottom: 0.5em; -} - -/*-------------- - Pointing - ---------------*/ - -.ui.pointing.dropdown > .menu { - top: 100%; - margin-top: 0.78571429rem; - border-radius: 0.28571429rem; -} -.ui.pointing.dropdown > .menu:not(.hidden):after { - display: block; - position: absolute; - pointer-events: none; - content: ''; - visibility: visible; - transform: rotate(45deg); - width: 0.5em; - height: 0.5em; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); - background: #FFFFFF; - z-index: 2; -} -.ui.pointing.dropdown > .menu:not(.hidden):after { - top: -0.25em; - left: 50%; - margin: 0 0 0 -0.25em; -} - -/* Top Left Pointing */ -.ui.top.left.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - left: 0; - right: auto; - margin: 1em 0 0; -} -.ui.top.left.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - left: 0; - right: auto; - margin: 1em 0 0; -} -.ui.top.left.pointing.dropdown > .menu:after { - top: -0.25em; - left: 1em; - right: auto; - margin: 0; - transform: rotate(45deg); -} - -/* Top Right Pointing */ -.ui.top.right.pointing.dropdown > .menu { - top: 100%; - bottom: auto; - right: 0; - left: auto; - margin: 1em 0 0; -} -.ui.top.pointing.dropdown > .left.menu:after, -.ui.top.right.pointing.dropdown > .menu:after { - top: -0.25em; - left: auto !important; - right: 1em !important; - margin: 0; - transform: rotate(45deg); -} - -/* Left Pointing */ -.ui.left.pointing.dropdown > .menu { - top: 0; - left: 100%; - right: auto; - margin: 0 0 0 1em; -} -.ui.left.pointing.dropdown > .menu:after { - top: 1em; - left: -0.25em; - margin: 0 0 0 0; - transform: rotate(-45deg); -} -.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu { - left: auto !important; - right: 100% !important; - margin: 0 1em 0 0; -} -.ui.left:not(.top):not(.bottom).pointing.dropdown > .left.menu:after { - top: 1em; - left: auto; - right: -0.25em; - margin: 0 0 0 0; - transform: rotate(135deg); -} - -/* Right Pointing */ -.ui.right.pointing.dropdown > .menu { - top: 0; - left: auto; - right: 100%; - margin: 0 1em 0 0; -} -.ui.right.pointing.dropdown > .menu:after { - top: 1em; - left: auto; - right: -0.25em; - margin: 0 0 0 0; - transform: rotate(135deg); -} - -/* Bottom Pointing */ -.ui.bottom.pointing.dropdown > .menu { - top: auto; - bottom: 100%; - left: 0; - right: auto; - margin: 0 0 1em; -} -.ui.bottom.pointing.dropdown > .menu:after { - top: auto; - bottom: -0.25em; - right: auto; - margin: 0; - transform: rotate(-135deg); -} - -/* Reverse Sub-Menu Direction */ -.ui.bottom.pointing.dropdown > .menu .menu { - top: auto !important; - bottom: 0 !important; -} - -/* Bottom Left */ -.ui.bottom.left.pointing.dropdown > .menu { - left: 0; - right: auto; -} -.ui.bottom.left.pointing.dropdown > .menu:after { - left: 1em; - right: auto; -} - -/* Bottom Right */ -.ui.bottom.right.pointing.dropdown > .menu { - right: 0; - left: auto; -} -.ui.bottom.right.pointing.dropdown > .menu:after { - left: auto; - right: 1em; -} - -/* Upward pointing */ -.ui.pointing.upward.dropdown .menu, -.ui.top.pointing.upward.dropdown .menu { - top: auto !important; - bottom: 100% !important; - margin: 0 0 0.78571429rem; - border-radius: 0.28571429rem; -} -.ui.pointing.upward.dropdown .menu:after, -.ui.top.pointing.upward.dropdown .menu:after { - top: 100% !important; - bottom: auto !important; - box-shadow: 1px 1px 0 0 rgba(34, 36, 38, 0.15); - margin: -0.25em 0 0; -} - -/* Right Pointing Upward */ -.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu { - top: auto !important; - bottom: 0 !important; - margin: 0 1em 0 0; -} -.ui.right.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after { - top: auto !important; - bottom: 0 !important; - margin: 0 0 1em 0; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/* Left Pointing Upward */ -.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu { - top: auto !important; - bottom: 0 !important; - margin: 0 0 0 1em; -} -.ui.left.pointing.upward.dropdown:not(.top):not(.bottom) .menu:after { - top: auto !important; - bottom: 0 !important; - margin: 0 0 1em 0; - box-shadow: -1px -1px 0 0 rgba(34, 36, 38, 0.15); -} - -/*-------------------- - Sizes ----------------------*/ - -.ui.dropdown, -.ui.dropdown .menu > .item { - font-size: 1rem; -} -.ui.mini.dropdown, -.ui.mini.dropdown .menu > .item { - font-size: 0.78571429rem; -} -.ui.tiny.dropdown, -.ui.tiny.dropdown .menu > .item { - font-size: 0.85714286rem; -} -.ui.small.dropdown, -.ui.small.dropdown .menu > .item { - font-size: 0.92857143rem; -} -.ui.large.dropdown, -.ui.large.dropdown .menu > .item { - font-size: 1.14285714rem; -} -.ui.big.dropdown, -.ui.big.dropdown .menu > .item { - font-size: 1.28571429rem; -} -.ui.huge.dropdown, -.ui.huge.dropdown .menu > .item { - font-size: 1.42857143rem; -} -.ui.massive.dropdown, -.ui.massive.dropdown .menu > .item { - font-size: 1.71428571rem; -} - - -/******************************* - Theme Overrides -*******************************/ - - -/* Dropdown Carets */ -@font-face { - font-family: 'Dropdown'; - src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjB5AAAAC8AAAAYGNtYXAPfuIIAAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zjo82LgAAAFwAAABVGhlYWQAQ88bAAACxAAAADZoaGVhAwcB6QAAAvwAAAAkaG10eAS4ABIAAAMgAAAAIGxvY2EBNgDeAAADQAAAABJtYXhwAAoAFgAAA1QAAAAgbmFtZVcZpu4AAAN0AAABRXBvc3QAAwAAAAAEvAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDX//3//wAB/+MPLQADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAIABJQElABMAABM0NzY3BTYXFhUUDwEGJwYvASY1AAUGBwEACAUGBoAFCAcGgAUBEgcGBQEBAQcECQYHfwYBAQZ/BwYAAQAAAG4BJQESABMAADc0PwE2MzIfARYVFAcGIyEiJyY1AAWABgcIBYAGBgUI/wAHBgWABwaABQWABgcHBgUFBgcAAAABABIASQC3AW4AEwAANzQ/ATYXNhcWHQEUBwYnBi8BJjUSBoAFCAcFBgYFBwgFgAbbBwZ/BwEBBwQJ/wgEBwEBB38GBgAAAAABAAAASQClAW4AEwAANxE0NzYzMh8BFhUUDwEGIyInJjUABQYHCAWABgaABQgHBgVbAQAIBQYGgAUIBwWABgYFBwAAAAEAAAABAADZuaKOXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAAAAACgAUAB4AQgBkAIgAqgAAAAEAAAAIABQAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAOAAAAAQAAAAAAAgAOAEcAAQAAAAAAAwAOACQAAQAAAAAABAAOAFUAAQAAAAAABQAWAA4AAQAAAAAABgAHADIAAQAAAAAACgA0AGMAAwABBAkAAQAOAAAAAwABBAkAAgAOAEcAAwABBAkAAwAOACQAAwABBAkABAAOAFUAAwABBAkABQAWAA4AAwABBAkABgAOADkAAwABBAkACgA0AGMAaQBjAG8AbQBvAG8AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AbgBSAGUAZwB1AGwAYQByAGkAYwBvAG0AbwBvAG4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'), url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAVwAAoAAAAABSgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAdkAAAHZLDXE/09TLzIAAALQAAAAYAAAAGAIIweQY21hcAAAAzAAAABMAAAATA9+4ghnYXNwAAADfAAAAAgAAAAIAAAAEGhlYWQAAAOEAAAANgAAADYAQ88baGhlYQAAA7wAAAAkAAAAJAMHAelobXR4AAAD4AAAACAAAAAgBLgAEm1heHAAAAQAAAAABgAAAAYACFAAbmFtZQAABAgAAAFFAAABRVcZpu5wb3N0AAAFUAAAACAAAAAgAAMAAAEABAQAAQEBCGljb21vb24AAQIAAQA6+BwC+BsD+BgEHgoAGVP/i4seCgAZU/+LiwwHi2v4lPh0BR0AAACIDx0AAACNER0AAAAJHQAAAdASAAkBAQgPERMWGyAlKmljb21vb25pY29tb29udTB1MXUyMHVGMEQ3dUYwRDh1RjBEOXVGMERBAAACAYkABgAIAgABAAQABwAKAA0AVgCfAOgBL/yUDvyUDvyUDvuUDvtvi/emFYuQjZCOjo+Pj42Qiwj3lIsFkIuQiY6Hj4iNhouGi4aJh4eHCPsU+xQFiIiGiYaLhouHjYeOCPsU9xQFiI+Jj4uQCA77b4v3FBWLkI2Pjo8I9xT3FAWPjo+NkIuQi5CJjogI9xT7FAWPh42Hi4aLhomHh4eIiIaJhosI+5SLBYaLh42HjoiPiY+LkAgO+92d928Vi5CNkI+OCPcU9xQFjo+QjZCLkIuPiY6Hj4iNhouGCIv7lAWLhomHh4iIh4eJhouGi4aNiI8I+xT3FAWHjomPi5AIDvvdi+YVi/eUBYuQjZCOjo+Pj42Qi5CLkImOhwj3FPsUBY+IjYaLhouGiYeHiAj7FPsUBYiHhomGi4aLh42Hj4iOiY+LkAgO+JQU+JQViwwKAAAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA8NoB4P/g/+AB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAA4AAAACgAIAAIAAgABACDw2v/9//8AAAAAACDw1//9//8AAf/jDy0AAwABAAAAAAAAAAAAAAABAAH//wAPAAEAAAABAAA5emozXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAUAAACAAAAAAADgCuAAEAAAAAAAEADgAAAAEAAAAAAAIADgBHAAEAAAAAAAMADgAkAAEAAAAAAAQADgBVAAEAAAAAAAUAFgAOAAEAAAAAAAYABwAyAAEAAAAAAAoANABjAAMAAQQJAAEADgAAAAMAAQQJAAIADgBHAAMAAQQJAAMADgAkAAMAAQQJAAQADgBVAAMAAQQJAAUAFgAOAAMAAQQJAAYADgA5AAMAAQQJAAoANABjAGkAYwBvAG0AbwBvAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGkAYwBvAG0AbwBvAG5pY29tb29uAGkAYwBvAG0AbwBvAG4AUgBlAGcAdQBsAGEAcgBpAGMAbwBtAG8AbwBuAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff'); - font-weight: normal; - font-style: normal; -} -.ui.dropdown > .dropdown.icon { - font-family: 'Dropdown'; - line-height: 1; - height: 1em; - width: 1.23em; - backface-visibility: hidden; - font-weight: normal; - font-style: normal; - text-align: center; -} -.ui.dropdown > .dropdown.icon { - width: auto; -} -.ui.dropdown > .dropdown.icon:before { - content: '\f0d7'; -} - -/* Sub Menu */ -.ui.dropdown .menu .item .dropdown.icon:before { - content: '\f0da' /*rtl:'\f0d9'*/; -} -.ui.dropdown .item .left.dropdown.icon:before, -.ui.dropdown .left.menu .item .dropdown.icon:before { - content: "\f0d9" /*rtl:"\f0da"*/; -} - -/* Vertical Menu Dropdown */ -.ui.vertical.menu .dropdown.item > .dropdown.icon:before { - content: "\f0da" /*rtl:"\f0d9"*/; -} -/* Icons for Reference -.dropdown.down.icon { - content: "\f0d7"; -} -.dropdown.up.icon { - content: "\f0d8"; -} -.dropdown.left.icon { - content: "\f0d9"; -} -.dropdown.icon.icon { - content: "\f0da"; -} -*/ - - -/******************************* - User Overrides -*******************************/ - diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 47d815490dc..b8f066db748 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -4158,7 +4158,7 @@ $.fn.dropdown.settings.templates = { html = '', escape = $.fn.dropdown.settings.templates.escape ; - html += ''; + html += ''; if(placeholder) { html += '
    ' + escape(placeholder,preserveHTML) + '
    '; } diff --git a/web_src/fomantic/build/fomantic.css b/web_src/fomantic/build/fomantic.css index e3dd4dcfe23..9b5f654a8eb 100644 --- a/web_src/fomantic/build/fomantic.css +++ b/web_src/fomantic/build/fomantic.css @@ -1,3 +1,2 @@ -@import "./components/dropdown.css"; @import "./components/modal.css"; @import "./components/search.css"; diff --git a/web_src/fomantic/semantic.json b/web_src/fomantic/semantic.json index a70bfdd16f9..e135528f8ff 100644 --- a/web_src/fomantic/semantic.json +++ b/web_src/fomantic/semantic.json @@ -22,7 +22,6 @@ "admin": false, "components": [ "api", - "dropdown", "modal", "search", "tab" From b24780b3a318dcb4abc4634761d8356f4f0d6076 Mon Sep 17 00:00:00 2001 From: yshyuk <43194469+yshyuk@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:25:23 +0900 Subject: [PATCH 13/50] Fix typos and grammar in English locale (#36751) Fix several English locale issues as suggested in #35015: - Rename `enterred` to `entered` in locale keys (`form.enterred_invalid_*`) and update all Go source references accordingly - Fix subject-verb agreement in `oauth2_applications_desc` and `oauth2_application_create_description` - Improve awkward phrasing in `startpage.license_desc` Only `locale_en-US.json` is modified; other locales are managed by Crowdin. Ref #35015 --------- Signed-off-by: yshyuk Co-authored-by: Claude Opus 4.6 --- options/locale/locale_en-US.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index a3dc09bb21d..8f7a050b16b 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -225,7 +225,7 @@ "startpage.lightweight": "Lightweight", "startpage.lightweight_desc": "Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!", "startpage.license": "Open Source", - "startpage.license_desc": "Go get %[2]s! Join us by contributing to make this project even better. Don't be shy to be a contributor!", + "startpage.license_desc": "Go get %[2]s! Join us by contributing to make this project even better. Don't hesitate to contribute!", "install.install": "Installation", "install.installing_desc": "Installing now, please wait…", "install.title": "Initial Configuration", @@ -866,7 +866,7 @@ "settings.permissions_list": "Permissions:", "settings.manage_oauth2_applications": "Manage OAuth2 Applications", "settings.edit_oauth2_application": "Edit OAuth2 Application", - "settings.oauth2_applications_desc": "OAuth2 applications enables your third-party application to securely authenticate users at this Gitea instance.", + "settings.oauth2_applications_desc": "OAuth2 applications enable your third-party application to securely authenticate users at this Gitea instance.", "settings.remove_oauth2_application": "Remove OAuth2 Application", "settings.remove_oauth2_application_desc": "Removing an OAuth2 application will revoke access to all signed access tokens. Continue?", "settings.remove_oauth2_application_success": "The application has been deleted.", @@ -885,7 +885,7 @@ "settings.oauth2_regenerate_secret_hint": "Lost your secret?", "settings.oauth2_client_secret_hint": "The secret will not be shown again after you leave or refresh this page. Please ensure that you have saved it.", "settings.oauth2_application_edit": "Edit", - "settings.oauth2_application_create_description": "OAuth2 applications gives your third-party application access to user accounts on this instance.", + "settings.oauth2_application_create_description": "OAuth2 applications give your third-party application access to user accounts on this instance.", "settings.oauth2_application_remove_description": "Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?", "settings.oauth2_application_locked": "Gitea pre-registers some OAuth2 applications on startup if enabled in config. To prevent unexpected behavior, these can neither be edited nor removed. Please refer to the OAuth2 documentation for more information.", "settings.authorized_oauth2_applications": "Authorized OAuth2 Applications", From 2e00b2f0bb7222998c04cb5b85dded8a198de583 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Feb 2026 23:23:21 +0100 Subject: [PATCH 14/50] Fix `no-content` message not rendering after comment edit (#36733) When non-empty comment content edited is deleted, it would render a empty comment body: image Fix it so it renders the same placeholder HTML that the server sends for empty content before edits: image --- routers/web/repo/issue.go | 10 +++++++++- routers/web/repo/issue_comment.go | 8 +------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index eaec3b57890..a295a3c9036 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -21,6 +21,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" @@ -369,7 +370,7 @@ func UpdateIssueContent(ctx *context.Context) { } ctx.JSON(http.StatusOK, map[string]any{ - "content": content, + "content": commentContentHTML(ctx, content), "contentVersion": issue.ContentVersion, "attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content), }) @@ -629,6 +630,13 @@ func updateAttachments(ctx *context.Context, item any, files []string) error { return err } +func commentContentHTML(ctx *context.Context, content template.HTML) template.HTML { + if strings.TrimSpace(string(content)) == "" { + return htmlutil.HTMLFormat(`%s`, ctx.Tr("repo.issues.no_content")) + } + return content +} + func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML { attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{ "ctxData": ctx.Data, diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index a3cb88e76a6..7f8cc23a3f9 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -9,7 +9,6 @@ import ( "html/template" "net/http" "strconv" - "strings" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -17,7 +16,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" repo_module "code.gitea.io/gitea/modules/repository" @@ -291,12 +289,8 @@ func UpdateCommentContent(ctx *context.Context) { } } - if strings.TrimSpace(string(renderedContent)) == "" { - renderedContent = htmlutil.HTMLFormat(`%s`, ctx.Tr("repo.issues.no_content")) - } - ctx.JSON(http.StatusOK, map[string]any{ - "content": renderedContent, + "content": commentContentHTML(ctx, renderedContent), "contentVersion": comment.ContentVersion, "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), }) From 3b250ba04e126f70ac6fa388ae90d508996ed21d Mon Sep 17 00:00:00 2001 From: xiaox <827812965@qq.com> Date: Sat, 28 Feb 2026 21:03:25 +0800 Subject: [PATCH 15/50] refactor: replace legacy tw-flex utility classes with flex-text-block/inline (#36778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Replace combinations of `tw-flex tw-items-center` (with optional `tw-gap-*`) with semantic `flex-text-block` or `flex-text-inline` classes across 15 template files. This follows the refactoring direction outlined in #35015 ("Refactor legacy `tw-flex tw-items-center tw-gap-xx` to `flex-text-block` or `flex-text-inline`"). ## Changes ### Replacement rules applied: - `tw-flex tw-items-center tw-gap-2` → `flex-text-block` (both have `gap: 0.5rem`) - `tw-flex tw-items-center tw-gap-1` → `flex-text-inline` (both have `gap: 0.25rem`) - `tw-flex tw-items-center` (no explicit gap) → `flex-text-block` where the element is block-level and children benefit from the default gap - `tw-flex tw-items-center` (inline context, e.g. ``, ``) → `flex-text-inline` ### Files modified (15): - `templates/admin/config.tmpl` — config page dt elements - `templates/admin/repo/unadopted.tmpl` — unadopted repo list items - `templates/base/head_navbar.tmpl` — active stopwatch popup - `templates/org/header.tmpl` — org header action buttons - `templates/org/home.tmpl` — member/team count links - `templates/org/settings/labels.tmpl` — labels page header - `templates/repo/branch/list.tmpl` — branch list header - `templates/repo/commits_table.tmpl` — commits table header - `templates/repo/diff/box.tmpl` — diff detail box - `templates/repo/diff/new_review.tmpl` — review form header - `templates/repo/issue/card.tmpl` — issue card unpin button - `templates/repo/issue/view_content/attachments.tmpl` — attachment file size - `templates/repo/migrate/migrate.tmpl` — migration service cards - `templates/shared/user/org_profile_avatar.tmpl` — org profile header - `templates/webhook/new.tmpl` — webhook type dropdown text ### What was NOT changed: - Elements with `tw-justify-between` or `tw-justify-center` (these need additional classes) - Elements whose children use explicit margins (`tw-mr-*`, `tw-ml-*`) that would conflict with the gap from flex-text classes - Fomantic UI form elements with special layout requirements ## Notes - This PR was created with AI assistance (Claude). All changes were reviewed individually to ensure semantic correctness and zero unintended visual changes. - No functional changes — purely CSS class refactoring. Closes: part of #35015 Signed-off-by: xiaox315 Co-authored-by: xiaox315 --- templates/admin/config.tmpl | 4 ++-- templates/admin/repo/unadopted.tmpl | 2 +- templates/base/head_navbar.tmpl | 4 ++-- templates/org/header.tmpl | 2 +- templates/org/home.tmpl | 4 ++-- templates/org/settings/labels.tmpl | 2 +- templates/repo/branch/list.tmpl | 2 +- templates/repo/commits_table.tmpl | 2 +- templates/repo/diff/box.tmpl | 2 +- templates/repo/diff/new_review.tmpl | 2 +- templates/repo/issue/card.tmpl | 2 +- templates/repo/issue/view_content/attachments.tmpl | 2 +- templates/repo/migrate/migrate.tmpl | 2 +- templates/shared/user/org_profile_avatar.tmpl | 2 +- templates/webhook/new.tmpl | 2 +- 15 files changed, 18 insertions(+), 18 deletions(-) diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index a61dec96203..6dc6e0d5ea1 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -223,7 +223,7 @@
    {{ctx.Locale.Tr "admin.config.mailer_user"}}
    {{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}
    -
    {{ctx.Locale.Tr "admin.config.send_test_mail"}}
    +
    {{ctx.Locale.Tr "admin.config.send_test_mail"}}
    @@ -254,7 +254,7 @@
    {{.CacheItemTTL}}
    {{end}}
    -
    {{ctx.Locale.Tr "admin.config.cache_test"}}
    +
    {{ctx.Locale.Tr "admin.config.cache_test"}}
    diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index 54b76c08bca..e66add6ce8e 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -20,7 +20,7 @@ {{if .Dirs}}
    {{range $dirI, $dir := .Dirs}} -
    +
    {{svg "octicon-file-directory-fill"}} {{$dir}}
    diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 28fcee023fc..43cbcbdc0ce 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -151,8 +151,8 @@ {{$activeStopwatch := and .PageGlobalData (call .PageGlobalData.GetActiveStopwatch)}} {{if $activeStopwatch}}
    -
    - +
    + {{svg "octicon-issue-opened" 16}} {{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}} diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 040164ba866..31f43449abc 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -7,7 +7,7 @@ {{if .Org.Visibility.IsLimited}}{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}{{end}} {{if .Org.Visibility.IsPrivate}}{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}{{end}} - + {{if .EnableFeed}} {{svg "octicon-rss" 24}} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index ea294980e04..ab088ce7b2d 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -49,7 +49,7 @@ {{if .NumMembers}}

    {{ctx.Locale.Tr "org.members"}} - {{.NumMembers}} {{svg "octicon-chevron-right"}} + {{.NumMembers}} {{svg "octicon-chevron-right"}}

    {{$isMember := .IsOrganizationMember}} @@ -63,7 +63,7 @@ {{if .IsOrganizationMember}}
    {{range .Teams}} diff --git a/templates/org/settings/labels.tmpl b/templates/org/settings/labels.tmpl index 21d7c0ef3c0..283b2199cb7 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,6 +1,6 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
    -
    +
    {{ctx.Locale.Tr "org.settings.labels_desc"}}
    diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 593b9d454d1..5e0d07ed6d9 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -71,7 +71,7 @@ {{end}}

    -
    +
    {{ctx.Locale.Tr "repo.branches"}}

    diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index c8ae535a182..8f6e6e01692 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -1,5 +1,5 @@

    -
    +
    {{if or .PageIsCommits (gt .CommitCount 0)}} {{.CommitCount}} {{ctx.Locale.Tr "repo.commits.commits"}} {{else if .IsNothingToCompare}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 41a8268cb32..3a152a7e81b 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,7 +1,7 @@ {{$showFileTree := (and (not .DiffNotAvailable) (gt .DiffShortStat.NumFiles 1))}}
    -
    +
    {{if $showFileTree}} {{else}} - {{$textNegitive := ctx.Locale.Tr "modal.no"}} + {{$textNegative := ctx.Locale.Tr "modal.no"}} {{$textPositive := ctx.Locale.Tr "modal.yes"}} {{if eq .ModalButtonTypes "confirm"}} - {{$textNegitive = ctx.Locale.Tr "modal.cancel"}} + {{$textNegative = ctx.Locale.Tr "modal.cancel"}} {{$textPositive = ctx.Locale.Tr "modal.confirm"}} {{end}} - {{if .ModalButtonCancelText}}{{$textNegitive = .ModalButtonCancelText}}{{end}} + {{if .ModalButtonCancelText}}{{$textNegative = .ModalButtonCancelText}}{{end}} {{if .ModalButtonOkText}}{{$textPositive = .ModalButtonOkText}}{{end}} - + {{end}}
    diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 09edcb11855..e1b7364f41c 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -77,7 +77,7 @@
    -
    +
    {{range .Columns}}
    diff --git a/templates/repo/diff/blob_excerpt.tmpl b/templates/repo/diff/blob_excerpt.tmpl index c9aac6d61d8..916d589839a 100644 --- a/templates/repo/diff/blob_excerpt.tmpl +++ b/templates/repo/diff/blob_excerpt.tmpl @@ -15,7 +15,7 @@ {{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}{{end}} {{if $line.LeftIdx}}{{end}} - {{/* ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: here it intentially use "right" side to comment, because the backend code depends on the assumption that the comment only happens on right side*/}} + {{/* ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: here it intentionally use "right" side to comment, because the backend code depends on the assumption that the comment only happens on right side*/}} {{- if and $canCreateComment $line.RightIdx -}}
    - + {{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} @@ -128,7 +128,7 @@ {{end}}
    - + {{svg "octicon-sign-out"}} {{ctx.Locale.Tr "sign_out"}} diff --git a/tests/integration/signout_test.go b/tests/integration/signout_test.go index 7fd0b5c64a0..0c0ac5dd87c 100644 --- a/tests/integration/signout_test.go +++ b/tests/integration/signout_test.go @@ -7,7 +7,10 @@ import ( "net/http" "testing" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" ) func TestSignOut(t *testing.T) { @@ -15,8 +18,9 @@ func TestSignOut(t *testing.T) { session := loginUser(t, "user2") - req := NewRequest(t, "POST", "/user/logout") - session.MakeRequest(t, req, http.StatusOK) + req := NewRequest(t, "GET", "/user/logout") + resp := session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/", test.RedirectURL(resp)) // try to view a private repo, should fail req = NewRequest(t, "GET", "/user2/repo2") From 3ee7a87c8aaea6201667afc6faaadaa706b6d525 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 07:56:23 +0000 Subject: [PATCH 20/50] Update Nix flake (#36787) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a608aa3b892..d00e8c85018 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1771369470, - "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0182a361324364ae3f436a63005877674cf45efb", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "type": "github" }, "original": { From e3cf3601540b721b9a564ad0320ca4021a75c8eb Mon Sep 17 00:00:00 2001 From: shafi-VM Date: Sun, 1 Mar 2026 14:41:25 +0530 Subject: [PATCH 21/50] =?UTF-8?q?Add=20=E2=80=9CCopy=20Source=E2=80=9D=20t?= =?UTF-8?q?o=20markup=20comment=20menu=20(#36726)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any user with **read access** to a comment can now copy its raw markdown source via the `···` context menu — no edit permission required. Closes #36722. --------- Signed-off-by: silverwind Co-authored-by: wxiaoguang Co-authored-by: silverwind Co-authored-by: Claude Opus 4.6 --- options/locale/locale_en-US.json | 1 + templates/repo/diff/comments.tmpl | 2 +- templates/repo/issue/view_content.tmpl | 2 +- templates/repo/issue/view_content/comments.tmpl | 4 ++-- .../repo/issue/view_content/context_menu.tmpl | 1 + .../repo/issue/view_content/conversation.tmpl | 2 +- web_src/js/features/clipboard.ts | 14 +++++++++++--- web_src/js/modules/tippy.ts | 9 +++++++++ 8 files changed, 27 insertions(+), 8 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 8f7a050b16b..0f27c8d82da 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1519,6 +1519,7 @@ "repo.issues.commented_at": "commented %s", "repo.issues.delete_comment_confirm": "Are you sure you want to delete this comment?", "repo.issues.context.copy_link": "Copy Link", + "repo.issues.context.copy_source": "Copy Source", "repo.issues.context.quote_reply": "Quote Reply", "repo.issues.context.reference_issue": "Reference in New Issue", "repo.issues.context.edit": "Edit", diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl index f1907b7ff1c..546a0229a78 100644 --- a/templates/repo/diff/comments.tmpl +++ b/templates/repo/diff/comments.tmpl @@ -64,7 +64,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 13e007ad952..047a9ac1251 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -52,7 +52,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Issue.Content}}
    +
    {{.Issue.Content}}
    {{if .Issue.Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Issue.Attachments "RenderedContent" .Issue.RenderedContent}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index a019c4bf3db..39fd8220985 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -67,7 +67,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} @@ -432,7 +432,7 @@ {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/templates/repo/issue/view_content/context_menu.tmpl b/templates/repo/issue/view_content/context_menu.tmpl index 749a2fa0ddc..e28d20a2716 100644 --- a/templates/repo/issue/view_content/context_menu.tmpl +++ b/templates/repo/issue/view_content/context_menu.tmpl @@ -10,6 +10,7 @@ {{$referenceUrl = printf "%s/files#%s" ctx.RootData.Issue.Link .item.HashTag}} {{end}}
    {{ctx.Locale.Tr "repo.issues.context.copy_link"}}
    +
    {{ctx.Locale.Tr "repo.issues.context.copy_source"}}
    {{if ctx.RootData.IsSigned}} {{$needDivider := false}} {{if not ctx.RootData.Repository.IsArchived}} diff --git a/templates/repo/issue/view_content/conversation.tmpl b/templates/repo/issue/view_content/conversation.tmpl index dd515933db4..333d120fde7 100644 --- a/templates/repo/issue/view_content/conversation.tmpl +++ b/templates/repo/issue/view_content/conversation.tmpl @@ -100,7 +100,7 @@ The variables in "ctx.Data" are different in each case, making this template fra {{ctx.Locale.Tr "repo.issues.no_content"}} {{end}}
    -
    {{.Content}}
    +
    {{.Content}}
    {{if .Attachments}} {{template "repo/issue/view_content/attachments" dict "Attachments" .Attachments "RenderedContent" .RenderedContent}} diff --git a/web_src/js/features/clipboard.ts b/web_src/js/features/clipboard.ts index d5e3b55f957..8dbaab62aac 100644 --- a/web_src/js/features/clipboard.ts +++ b/web_src/js/features/clipboard.ts @@ -6,7 +6,7 @@ const {copy_success, copy_error} = window.config.i18n; // Enable clipboard copy from HTML attributes. These properties are supported: // - data-clipboard-text: Direct text to copy -// - data-clipboard-target: Holds a selector for a or