Merge remote-tracking branch 'upstream/main' into ssh-mirroring

This commit is contained in:
techknowlogick
2025-08-08 12:13:01 -04:00
266 changed files with 5415 additions and 7386 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
.admin code,
.admin pre {
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.admin .ui.table.segment {
+1 -1
View File
@@ -20,7 +20,7 @@
background: var(--color-card);
border: 1px solid var(--color-secondary);
box-shadow: none;
word-wrap: break-word;
overflow-wrap: break-word;
border-radius: var(--border-radius);
}
+1 -1
View File
@@ -83,6 +83,6 @@
.ui.comments .comment .text {
margin: 0.25em 0 0.5em;
font-size: 1em;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.3;
}
+4 -12
View File
@@ -141,7 +141,7 @@ td .commit-summary {
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
gap: 0.25em;
gap: 0.5em;
}
@media (max-width: 767.98px) {
@@ -334,7 +334,7 @@ td .commit-summary {
display: flex;
gap: 0.5em;
margin-bottom: 8px;
min-height: 40px; /* avoid layout shift on edit */
min-height: 36px; /* avoid layout shift on edit */
}
.repository.view.issue .issue-title h1 {
@@ -342,20 +342,12 @@ td .commit-summary {
width: 100%;
font-weight: var(--font-weight-normal);
font-size: 32px;
line-height: 40px;
line-height: 36px; /* vertically center single-line text with .issue-title-buttons */
margin: 0;
padding-right: 0.25rem;
overflow-wrap: anywhere;
}
.repository.view.issue .issue-title#issue-title-display .issue-title-buttons {
margin-top: 4px; /* the title's height is 40px, fine tune to align the buttons */
}
.repository.view.issue .issue-title#issue-title-editor {
padding-top: 4px;
}
@media (max-width: 767.98px) {
.repository.view.issue .issue-title {
flex-direction: column;
@@ -1680,7 +1672,7 @@ tbody.commit-list {
.diff-file-header .file-link {
max-width: fit-content;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-box-orient: vertical; /* stylelint-disable property-no-deprecated -- https://github.com/stylelint/stylelint/issues/8698 */
-webkit-line-clamp: 2;
overflow: hidden;
overflow-wrap: anywhere;
+25 -18
View File
@@ -32,6 +32,7 @@ export default defineComponent({
locale: {
filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
} as Record<string, string>,
mergeBase: el.getAttribute('data-merge-base'),
commits: [] as Array<Commit>,
hoverActivated: false,
lastReviewCommitSha: '',
@@ -176,32 +177,38 @@ export default defineComponent({
}
},
/**
* When a commit is clicked with shift this enables the range
* selection. Second click (with shift) defines the end of the
* range. This opens the diff of this range
* Exception: first commit is the first commit of this PR. Then
* the diff from beginning of PR up to the second clicked commit is
* opened
* When a commit is clicked while holding Shift, it enables range selection.
* - The range selection is a half-open, half-closed range, meaning it excludes the start commit but includes the end commit.
* - The start of the commit range is always the previous commit of the first clicked commit.
* - If the first commit in the list is clicked, the mergeBase will be used as the start of the range instead.
* - The second Shift-click defines the end of the range.
* - Once both are selected, the diff view for the selected commit range will open.
*/
commitClickedShift(commit: Commit) {
this.hoverActivated = !this.hoverActivated;
commit.selected = true;
// Second click -> determine our range and open links accordingly
if (!this.hoverActivated) {
// since at least one commit is selected, we can determine the range
// find all selected commits and generate a link
if (this.commits[0].selected) {
// first commit is selected - generate a short url with only target sha
const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);
if (lastCommitIdx === this.commits.length - 1) {
// user selected all commits - just show the normal diff page
window.location.assign(`${this.issueLink}/files${this.queryParams}`);
} else {
window.location.assign(`${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`);
}
const firstSelected = this.commits.findIndex((x) => x.selected);
const lastSelected = this.commits.findLastIndex((x) => x.selected);
let beforeCommitID: string;
if (firstSelected === 0) {
beforeCommitID = this.mergeBase;
} else {
const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;
const end = this.commits.findLast((x) => x.selected).id;
window.location.assign(`${this.issueLink}/files/${start}..${end}${this.queryParams}`);
beforeCommitID = this.commits[firstSelected - 1].id;
}
const afterCommitID = this.commits[lastSelected].id;
if (firstSelected === lastSelected) {
// if the start and end are the same, we show this single commit
window.location.assign(`${this.issueLink}/commits/${afterCommitID}${this.queryParams}`);
} else if (beforeCommitID === this.mergeBase && afterCommitID === this.commits.at(-1).id) {
// if the first commit is selected and the last commit is selected, we show all commits
window.location.assign(`${this.issueLink}/files${this.queryParams}`);
} else {
window.location.assign(`${this.issueLink}/files/${beforeCommitID}..${afterCommitID}${this.queryParams}`);
}
}
},
@@ -58,8 +58,8 @@ onMounted(() => {
<template>
<div>
<div class="activity-bar-graph" ref="styleElement" style="width: 0; height: 0;"/>
<div class="activity-bar-graph-alt" ref="altStyleElement" style="width: 0; height: 0;"/>
<div class="activity-bar-graph tw-w-0 tw-h-0" ref="styleElement"/>
<div class="activity-bar-graph-alt tw-w-0 tw-h-0" ref="altStyleElement"/>
<vue-bar-graph
:points="graphPoints"
:show-x-axis="true"
+6 -1
View File
@@ -542,7 +542,12 @@ function initIssueTemplateCommentEditors(commentForm: HTMLFormElement) {
// deactivate all markdown editors
showElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-real'));
hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .combo-markdown-editor'));
hideElem(commentForm.querySelectorAll('.combo-editor-dropzone .form-field-dropzone'));
queryElems(commentForm, '.combo-editor-dropzone .form-field-dropzone', (dropzoneContainer) => {
// if "form-field-dropzone" exists, then "dropzone" must also exist
const dropzone = dropzoneContainer.querySelector<HTMLElement>('.dropzone').dropzone;
const hasUploadedFiles = dropzone.files.length !== 0;
toggleElem(dropzoneContainer, hasUploadedFiles);
});
// activate this markdown editor
hideElem(fieldTextarea);
+6 -2
View File
@@ -34,8 +34,12 @@ export function initRepoMigration() {
elCloneAddr.addEventListener('input', () => {
if (repoNameChanged) return;
let repoNameFromUrl = elCloneAddr.value.split(/[?#]/)[0];
repoNameFromUrl = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl)[3];
repoNameFromUrl = repoNameFromUrl.split(/[?#]/)[0];
const parts = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl);
if (!parts || parts.length < 4) {
elRepoName.value = '';
return;
}
repoNameFromUrl = parts[3].split(/[?#]/)[0];
elRepoName.value = sanitizeRepoName(repoNameFromUrl);
});
}
+1 -1
View File
@@ -42,7 +42,7 @@ type ToastOpts = {
type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast };
// See https://github.com/apvarun/toastify-js#api for options
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
const body = useHtmlBody ? message : htmlEscape(message);
const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
-1
View File
@@ -3,7 +3,6 @@ import {extname} from '../../utils.ts';
// support common 3D model file formats, use online-3d-viewer library for rendering
// eslint-disable-next-line multiline-comment-style
/* a simple text STL file example:
solid SimpleTriangle
facet normal 0 0 1
+14 -14
View File
@@ -2,19 +2,19 @@ import {decode, encode} from 'uint8-to-base64';
import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts';
import {toggleElemClass, toggleElem} from './utils/dom.ts';
// transform /path/to/file.ext to /path/to
/** transform /path/to/file.ext to /path/to */
export function dirname(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
return lastSlashIndex < 0 ? '' : path.substring(0, lastSlashIndex);
}
// transform /path/to/file.ext to file.ext
/** transform /path/to/file.ext to file.ext */
export function basename(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1);
}
// transform /path/to/file.ext to .ext
/** transform /path/to/file.ext to .ext */
export function extname(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
const lastPointIndex = path.lastIndexOf('.');
@@ -22,18 +22,18 @@ export function extname(path: string): string {
return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
}
// test whether a variable is an object
/** test whether a variable is an object */
export function isObject(obj: any): boolean {
return Object.prototype.toString.call(obj) === '[object Object]';
}
// returns whether a dark theme is enabled
/** returns whether a dark theme is enabled */
export function isDarkTheme(): boolean {
const style = window.getComputedStyle(document.documentElement);
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
}
// strip <tags> from a string
/** strip <tags> from a string */
export function stripTags(text: string): string {
return text.replace(/<[^>]*>?/g, '');
}
@@ -62,27 +62,27 @@ export function parseIssuePageInfo(): IssuePageInfo {
};
}
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
/** parse a URL, either relative '/path' or absolute 'https://localhost/path' */
export function parseUrl(str: string): URL {
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
}
// return current locale chosen by user
/** return current locale chosen by user */
export function getCurrentLocale(): string {
return document.documentElement.lang;
}
// given a month (0-11), returns it in the documents language
/** given a month (0-11), returns it in the documents language */
export function translateMonth(month: number) {
return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'});
}
// given a weekday (0-6, Sunday to Saturday), returns it in the documents language
/** given a weekday (0-6, Sunday to Saturday), returns it in the documents language */
export function translateDay(day: number) {
return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'});
}
// convert a Blob to a DataURI
/** convert a Blob to a DataURI */
export function blobToDataURI(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
try {
@@ -100,7 +100,7 @@ export function blobToDataURI(blob: Blob): Promise<string> {
});
}
// convert image Blob to another mime-type format.
/** convert image Blob to another mime-type format. */
export function convertImage(blob: Blob, mime: string): Promise<Blob> {
return new Promise(async (resolve, reject) => {
try {
@@ -143,7 +143,7 @@ export function toAbsoluteUrl(url: string): string {
return `${window.location.origin}${url}`;
}
// Encode an Uint8Array into a URLEncoded base64 string.
/** Encode an Uint8Array into a URLEncoded base64 string. */
export function encodeURLEncodedBase64(uint8Array: Uint8Array): string {
return encode(uint8Array)
.replace(/\+/g, '-')
@@ -151,7 +151,7 @@ export function encodeURLEncodedBase64(uint8Array: Uint8Array): string {
.replace(/=/g, '');
}
// Decode a URLEncoded base64 to an Uint8Array.
/** Decode a URLEncoded base64 to an Uint8Array. */
export function decodeURLEncodedBase64(base64url: string): Uint8Array {
return decode(base64url
.replace(/_/g, '/')
+4 -3
View File
@@ -1,7 +1,7 @@
import tinycolor from 'tinycolor2';
import type {ColorInput} from 'tinycolor2';
// Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance
/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */
// Keep this in sync with modules/util/color.go
function getRelativeLuminance(color: ColorInput): number {
const {r, g, b} = tinycolor(color).toRgb();
@@ -12,8 +12,9 @@ function useLightText(backgroundColor: ColorInput): boolean {
return getRelativeLuminance(backgroundColor) < 0.453;
}
// Given a background color, returns a black or white foreground color that the highest
// contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better.
/** Given a background color, returns a black or white foreground color that the highest
* contrast ratio. */
// In the future, the APCA contrast function, or CSS `contrast-color` will be better.
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
export function contrastColor(backgroundColor: ColorInput): string {
return useLightText(backgroundColor) ? '#fff' : '#000';
+10 -10
View File
@@ -71,7 +71,7 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
}), fn);
}
// it works like jQuery.children: only the direct children are selected
/** it works like jQuery.children: only the direct children are selected */
export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
if (isInFrontendUnitTest()) {
// https://github.com/capricorn86/happy-dom/issues/1620 : ":scope" doesn't work
@@ -81,7 +81,7 @@ export function queryElemChildren<T extends Element>(parent: Element | ParentNod
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
// it works like parent.querySelectorAll: all descendants are selected
/** it works like parent.querySelectorAll: all descendants are selected */
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent if the targets are not for page-level components.
export function queryElems<T extends HTMLElement>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
@@ -95,8 +95,8 @@ export function onDomReady(cb: () => Promisable<void>) {
}
}
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
/** checks whether an element is owned by the current document, and whether it is a document fragment or element node
* if it is, it means it is a "normal" element managed by us, which can be modified safely. */
export function isDocumentFragmentOrElementNode(el: Node) {
try {
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
@@ -106,8 +106,8 @@ export function isDocumentFragmentOrElementNode(el: Node) {
}
}
// autosize a textarea to fit content. Based on
// https://github.com/github/textarea-autosize
/** autosize a textarea to fit content. */
// Based on https://github.com/github/textarea-autosize
// ---------------------------------------------------------------------
// Copyright (c) 2018 GitHub, Inc.
//
@@ -246,8 +246,8 @@ export function onInputDebounce(fn: () => Promisable<any>) {
type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement;
// Set the `src` attribute on an element and returns a promise that resolves once the element
// has loaded or errored.
/** Set the `src` attribute on an element and returns a promise that resolves once the element
* has loaded or errored. */
export function loadElem(el: LoadableElement, src: string) {
return new Promise((resolve) => {
el.addEventListener('load', () => resolve(true), {once: true});
@@ -286,7 +286,7 @@ export function isElemVisible(el: HTMLElement): boolean {
return !el.classList.contains('tw-hidden') && (el.offsetWidth || el.offsetHeight || el.getClientRects().length) && el.style.display !== 'none';
}
// replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this
/** replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this */
export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) {
const before = textarea.value.slice(0, textarea.selectionStart ?? undefined);
const after = textarea.value.slice(textarea.selectionEnd ?? undefined);
@@ -368,7 +368,7 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event
}, options);
}
// Returns whether a click event is a left-click without any modifiers held
/** Returns whether a click event is a left-click without any modifiers held */
export function isPlainClick(e: MouseEvent) {
return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
}
+2 -2
View File
@@ -29,8 +29,8 @@ type ImageInfo = {
dppx?: number,
}
// decode a image and try to obtain width and dppx. It will never throw but instead
// return default values.
/** decode a image and try to obtain width and dppx. It will never throw but instead
* return default values. */
export async function imageInfo(blob: Blob): Promise<ImageInfo> {
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens
+2 -2
View File
@@ -65,8 +65,8 @@ export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataO
let dateFormat: Intl.DateTimeFormat;
// 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.
/** 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: Date | number): string {
if (!dateFormat) {
// TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
+2 -2
View File
@@ -14,8 +14,8 @@ export function isUrl(url: string): boolean {
}
}
// Convert an absolute or relative URL to an absolute URL with the current origin. It only
// processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'.
/** Convert an absolute or relative URL to an absolute URL with the current origin. It only
* processes absolute HTTP/HTTPS URLs or relative URLs like '/xxx' or '//host/xxx'. */
export function toOriginUrl(urlStr: string) {
try {
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {