diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl
index 3c21a42421..4e0f0e9c3e 100644
--- a/templates/repo/settings/webhook/history.tmpl
+++ b/templates/repo/settings/webhook/history.tmpl
@@ -19,6 +19,8 @@
{{if .IsSucceed}}
{{svg "octicon-check"}}
+ {{else if not .IsDelivered}}
+
{{svg "octicon-stopwatch"}}
{{else}}
{{svg "octicon-alert"}}
{{end}}
@@ -62,7 +64,7 @@
{{range $key, $val := .RequestInfo.Headers}}
{{$key}}: {{$val}}
{{end}}
{{ctx.Locale.Tr "repo.settings.webhook.payload"}}
-
{{.PayloadContent}}
+
{{or .RequestInfo.Body .PayloadContent}}
{{else}}
-
{{end}}
diff --git a/tests/integration/db_collation_test.go b/tests/integration/db_collation_test.go
index 468d13508d..75a4c1594f 100644
--- a/tests/integration/db_collation_test.go
+++ b/tests/integration/db_collation_test.go
@@ -22,12 +22,6 @@ type TestCollationTbl struct {
func TestDatabaseCollation(t *testing.T) {
x := db.GetEngine(db.DefaultContext).(*xorm.Engine)
- // there are blockers for MSSQL to use case-sensitive collation, see the comments in db/collation.go
- if setting.Database.Type.IsMSSQL() {
- t.Skip("there are blockers for MSSQL to use case-sensitive collation")
- return
- }
-
// all created tables should use case-sensitive collation by default
_, _ = x.Exec("DROP TABLE IF EXISTS test_collation_tbl")
err := x.Sync(&TestCollationTbl{})
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 77359b36e5..3db9cd894c 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1448,7 +1448,9 @@ strong.attention-caution, span.attention-caution {
}
.ui.label,
-.ui.menu .item > .label {
+.ui.menu .item > .label,
+.ui.grey.labels .label,
+.ui.ui.ui.grey.label {
background: var(--color-label-bg);
color: var(--color-label-text);
}
diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css
index 5137e0774c..fbe2458ed6 100644
--- a/web_src/css/themes/theme-gitea-light.css
+++ b/web_src/css/themes/theme-gitea-light.css
@@ -228,9 +228,9 @@
--color-nav-hover-bg: #ebebeb;
--color-nav-text: var(--color-text);
--color-label-text: var(--color-text);
- --color-label-bg: #cacaca5b;
- --color-label-hover-bg: #cacacaa0;
- --color-label-active-bg: #cacacaff;
+ --color-label-bg: #9d9d9d4b;
+ --color-label-hover-bg: #9d9d9da0;
+ --color-label-active-bg: #9d9d9dff;
--color-accent: var(--color-primary-light-1);
--color-small-accent: var(--color-primary-light-6);
--color-active-line: #fffbdd;
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 97cd05b45b..de9625b143 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -3,7 +3,7 @@ import {SvgIcon} from '../svg.js';
import ActionRunStatus from './ActionRunStatus.vue';
import {createApp} from 'vue';
import {toggleElem} from '../utils/dom.js';
-import {getCurrentLocale} from '../utils.js';
+import {formatDatetime} from '../utils/time.js';
import {renderAnsi} from '../render/ansi.js';
import {POST, DELETE} from '../modules/fetch.js';
@@ -167,7 +167,7 @@ const sfc = {
const logTimeStamp = document.createElement('span');
logTimeStamp.className = 'log-time-stamp';
const date = new Date(parseFloat(line.timestamp * 1000));
- const timeStamp = date.toLocaleString(getCurrentLocale(), {timeZoneName: 'short'});
+ const timeStamp = formatDatetime(date);
logTimeStamp.textContent = timeStamp;
toggleElem(logTimeStamp, this.timeVisible['log-time-stamp']);
// for "Show seconds"
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index 211253ef9a..ee4ade1f04 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -105,8 +105,10 @@ async function fetchActionDoRequest(actionElem, url, opt) {
showErrorToast(`server error: ${resp.status}`);
}
} catch (e) {
- console.error('error when doRequest', e);
- showErrorToast(`${i18n.network_error} ${e}`);
+ if (e.name !== 'AbortError') {
+ console.error('error when doRequest', e);
+ showErrorToast(`${i18n.network_error} ${e}`);
+ }
}
actionElem.classList.remove('is-loading', 'small-loading-icon');
}
diff --git a/web_src/js/features/repo-editor.js b/web_src/js/features/repo-editor.js
index f00f817223..4fe7ed8a4d 100644
--- a/web_src/js/features/repo-editor.js
+++ b/web_src/js/features/repo-editor.js
@@ -4,15 +4,14 @@ import {createCodeEditor} from './codeeditor.js';
import {hideElem, showElem} from '../utils/dom.js';
import {initMarkupContent} from '../markup/content.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
-
-const {csrfToken} = window.config;
+import {POST} from '../modules/fetch.js';
function initEditPreviewTab($form) {
const $tabMenu = $form.find('.tabular.menu');
$tabMenu.find('.item').tab();
const $previewTab = $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`);
if ($previewTab.length) {
- $previewTab.on('click', function () {
+ $previewTab.on('click', async function () {
const $this = $(this);
let context = `${$this.data('context')}/`;
const mode = $this.data('markup-mode') || 'comment';
@@ -21,43 +20,30 @@ function initEditPreviewTab($form) {
context += treePathEl.val();
}
context = context.substring(0, context.lastIndexOf('/'));
- $.post($this.data('url'), {
- _csrf: csrfToken,
- mode,
- context,
- text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
- file_path: treePathEl.val(),
- }, (data) => {
+
+ const formData = new FormData();
+ formData.append('mode', mode);
+ formData.append('context', context);
+ formData.append('text', $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val());
+ formData.append('file_path', treePathEl.val());
+ try {
+ const response = await POST($this.data('url'), {data: formData});
+ const data = await response.text();
const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`);
renderPreviewPanelContent($previewPanel, data);
- });
+ } catch (error) {
+ console.error('Error:', error);
+ }
});
}
}
-function initEditDiffTab($form) {
- const $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
- $tabMenu.find(`.item[data-tab="${$tabMenu.data('diff')}"]`).on('click', function () {
- const $this = $(this);
- $.post($this.data('url'), {
- _csrf: csrfToken,
- context: $this.data('context'),
- content: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val(),
- }, (data) => {
- const $diffPreviewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('diff')}"]`);
- $diffPreviewPanel.html(data);
- });
- });
-}
-
function initEditorForm() {
if ($('.repository .edit.form').length === 0) {
return;
}
initEditPreviewTab($('.repository .edit.form'));
- initEditDiffTab($('.repository .edit.form'));
}
function getCursorPosition($e) {
diff --git a/web_src/js/markup/anchors.js b/web_src/js/markup/anchors.js
index 53dfa2980c..03934ea215 100644
--- a/web_src/js/markup/anchors.js
+++ b/web_src/js/markup/anchors.js
@@ -2,6 +2,7 @@ import {svg} from '../svg.js';
const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6';
+// scroll to anchor while respecting the `user-content` prefix that exists on the target
function scrollToAnchor(hash, initial) {
// abort if the browser has already scrolled to another anchor during page load
if (initial && document.querySelector(':target')) return;
@@ -19,6 +20,7 @@ function scrollToAnchor(hash, initial) {
export function initMarkupAnchors() {
if (!document.querySelector('.markup')) return;
+ // create link icons for markup headings, the resulting link href will remove `user-content-`
for (const heading of document.querySelectorAll(headingSelector)) {
const originalId = heading.id.replace(/^user-content-/, '');
const a = document.createElement('a');
@@ -31,5 +33,18 @@ export function initMarkupAnchors() {
heading.prepend(a);
}
+ // handle user-defined `name` anchors like `[Link](#link)` linking to `
Link`
+ for (const a of document.querySelectorAll('.markup a[href^="#"]')) {
+ const href = a.getAttribute('href');
+ if (!href.startsWith('#user-content-')) continue;
+ const originalId = href.replace(/^#user-content-/, '');
+ a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
+ if (document.getElementsByName(originalId).length !== 1) {
+ a.addEventListener('click', (e) => {
+ scrollToAnchor(e.currentTarget.getAttribute('href'), false);
+ });
+ }
+ }
+
scrollToAnchor(window.location.hash, true);
}
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 27f371fd88..489afc0ae1 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -1,5 +1,6 @@
import tippy, {followCursor} from 'tippy.js';
import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
+import {formatDatetime} from '../utils/time.js';
const visibleInstances = new Set();
@@ -93,8 +94,15 @@ function attachTooltip(target, content = null) {
}
function switchTitleToTooltip(target) {
- const title = target.getAttribute('title');
+ let title = target.getAttribute('title');
if (title) {
+ // apply custom formatting to relative-time's tooltips
+ if (target.tagName.toLowerCase() === 'relative-time') {
+ const datetime = target.getAttribute('datetime');
+ if (datetime) {
+ title = formatDatetime(new Date(datetime));
+ }
+ }
target.setAttribute('data-tooltip-content', title);
target.setAttribute('aria-label', title);
// keep the attribute, in case there are some other "[title]" selectors
diff --git a/web_src/js/utils/time.js b/web_src/js/utils/time.js
index 3284e893e1..1848792c98 100644
--- a/web_src/js/utils/time.js
+++ b/web_src/js/utils/time.js
@@ -1,4 +1,5 @@
import dayjs from 'dayjs';
+import {getCurrentLocale} from '../utils.js';
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
export function startDaysBetween(startDate, endDate) {
@@ -44,3 +45,23 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) {
return Object.values(result);
}
+
+let dateFormat;
+
+// format a Date object to document's locale, but with 24h format from user's current locale because this
+// option is a personal preference of the user, not something that the document's locale should dictate.
+export function formatDatetime(date) {
+ if (!dateFormat) {
+ // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
+ dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ hour: 'numeric',
+ hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())),
+ minute: '2-digit',
+ timeZoneName: 'short',
+ });
+ }
+ return dateFormat.format(date);
+}