aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsilverwind <me@silverwind.io>2024-08-28 18:32:38 +0200
committerGitHub <noreply@github.com>2024-08-28 18:32:38 +0200
commit7207d93f01f0cd796e1b9e0156798990846d894b (patch)
tree3e3ffb93688c907975d36da1486dbcc3b46034a0
parent39d2fdefaf0dd42aa5e3ee8d3ea0a84b40c005f5 (diff)
downloadgitea-7207d93f01f0cd796e1b9e0156798990846d894b.tar.gz
gitea-7207d93f01f0cd796e1b9e0156798990846d894b.zip
Fix a number of Typescript issues (#31877)
Typescript error count is reduced from 633 to 540 with this. No runtime changes except in test code.
-rw-r--r--types.d.ts50
-rw-r--r--web_src/js/htmx.ts9
-rw-r--r--web_src/js/index.ts4
-rw-r--r--web_src/js/render/ansi.ts4
-rw-r--r--web_src/js/standalone/swagger.ts2
-rw-r--r--web_src/js/svg.test.ts2
-rw-r--r--web_src/js/types.ts7
-rw-r--r--web_src/js/utils.test.ts15
-rw-r--r--web_src/js/utils.ts47
-rw-r--r--web_src/js/utils/color.ts8
-rw-r--r--web_src/js/utils/dom.ts4
-rw-r--r--web_src/js/utils/image.ts16
-rw-r--r--web_src/js/utils/match.ts8
-rw-r--r--web_src/js/utils/time.ts24
-rw-r--r--web_src/js/utils/url.ts6
-rw-r--r--web_src/js/vitest.setup.ts12
16 files changed, 140 insertions, 78 deletions
diff --git a/types.d.ts b/types.d.ts
index a8dc09e064..68081af606 100644
--- a/types.d.ts
+++ b/types.d.ts
@@ -10,22 +10,52 @@ declare module '*.css' {
declare let __webpack_public_path__: string;
+declare module 'htmx.org/dist/htmx.esm.js' {
+ const value = await import('htmx.org');
+ export default value;
+}
+
+declare module 'uint8-to-base64' {
+ export function encode(arrayBuffer: ArrayBuffer): string;
+ export function decode(base64str: string): ArrayBuffer;
+}
+
+declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
+ const value = await import('swagger-ui-dist');
+ export default value.SwaggerUIBundle;
+}
+
+interface JQuery {
+ api: any, // fomantic
+ areYouSure: any, // jquery.are-you-sure
+ dimmer: any, // fomantic
+ dropdown: any; // fomantic
+ modal: any; // fomantic
+ tab: any; // fomantic
+ transition: any, // fomantic
+}
+
+interface JQueryStatic {
+ api: any, // fomantic
+}
+
+interface Element {
+ _tippy: import('tippy.js').Instance;
+}
+
+type Writable<T> = { -readonly [K in keyof T]: T[K] };
+
interface Window {
config: import('./web_src/js/types.ts').Config;
$: typeof import('@types/jquery'),
jQuery: typeof import('@types/jquery'),
- htmx: typeof import('htmx.org'),
+ htmx: Omit<typeof import('htmx.org/dist/htmx.esm.js').default, 'config'> & {
+ config?: Writable<typeof import('htmx.org').default.config>,
+ },
+ ui?: any,
_globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & {
_inited: boolean,
push: (e: ErrorEvent & PromiseRejectionEvent) => void | number,
},
-}
-
-declare module 'htmx.org/dist/htmx.esm.js' {
- const value = await import('htmx.org');
- export default value;
-}
-
-interface Element {
- _tippy: import('tippy.js').Instance;
+ __webpack_public_path__: string;
}
diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts
index bfc2147736..d4f317ee5a 100644
--- a/web_src/js/htmx.ts
+++ b/web_src/js/htmx.ts
@@ -1,20 +1,21 @@
import {showErrorToast} from './modules/toast.ts';
+import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx
+import type {HtmxResponseInfo} from 'htmx.org';
-// https://github.com/bigskysoftware/idiomorph#htmx
-import 'idiomorph/dist/idiomorph-ext.js';
+type HtmxEvent = Event & {detail: HtmxResponseInfo};
// https://htmx.org/reference/#config
window.htmx.config.requestClass = 'is-loading';
window.htmx.config.scrollIntoViewOnBoost = false;
// https://htmx.org/events/#htmx:sendError
-document.body.addEventListener('htmx:sendError', (event) => {
+document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => {
// TODO: add translations
showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
});
// https://htmx.org/events/#htmx:responseError
-document.body.addEventListener('htmx:responseError', (event) => {
+document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => {
// TODO: add translations
showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
});
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 2bdc8655fe..db678a25ba 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -98,12 +98,12 @@ initGiteaFomantic();
initDirAuto();
initSubmitEventPolyfill();
-function callInitFunctions(functions) {
+function callInitFunctions(functions: (() => any)[]) {
// Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1"
// It is a quick check, no side effect so no need to do slow URL parsing.
const initStart = performance.now();
if (window.location.search.includes('_ui_performance_trace=1')) {
- let results = [];
+ let results: {name: string, dur: number}[] = [];
for (const func of functions) {
const start = performance.now();
func();
diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts
index bb622dd1eb..685e916c9a 100644
--- a/web_src/js/render/ansi.ts
+++ b/web_src/js/render/ansi.ts
@@ -1,12 +1,12 @@
import {AnsiUp} from 'ansi_up';
-const replacements = [
+const replacements: Array<[RegExp, string]> = [
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
];
// render ANSI to HTML
-export function renderAnsi(line) {
+export function renderAnsi(line: string): string {
// create a fresh ansi_up instance because otherwise previous renders can influence
// the output of future renders, because ansi_up is stateful and remembers things like
// unclosed opening tags for colors.
diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts
index 2928813167..63b676b2ea 100644
--- a/web_src/js/standalone/swagger.ts
+++ b/web_src/js/standalone/swagger.ts
@@ -8,7 +8,7 @@ window.addEventListener('load', async () => {
// Make the page's protocol be at the top of the schemes list
const proto = window.location.protocol.slice(0, -1);
- spec.schemes.sort((a, b) => {
+ spec.schemes.sort((a: string, b: string) => {
if (a === proto) return -1;
if (b === proto) return 1;
return 0;
diff --git a/web_src/js/svg.test.ts b/web_src/js/svg.test.ts
index 015758a271..7f3e0496ec 100644
--- a/web_src/js/svg.test.ts
+++ b/web_src/js/svg.test.ts
@@ -17,7 +17,7 @@ test('svgParseOuterInner', () => {
test('SvgIcon', () => {
const root = document.createElement('div');
createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
- const node = root.firstChild;
+ const node = root.firstChild as Element;
expect(node.nodeName).toEqual('svg');
expect(node.getAttribute('width')).toEqual('24');
expect(node.getAttribute('height')).toEqual('24');
diff --git a/web_src/js/types.ts b/web_src/js/types.ts
index 3bd1c072a8..f3ac305162 100644
--- a/web_src/js/types.ts
+++ b/web_src/js/types.ts
@@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams;
export type RequestOpts = {
data?: RequestData,
} & RequestInit;
+
+export type IssueData = {
+ owner: string,
+ repo: string,
+ type: string,
+ index: string,
+}
diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts
index 4c09f49ba8..55896706ff 100644
--- a/web_src/js/utils.test.ts
+++ b/web_src/js/utils.test.ts
@@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => {
});
test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
- // TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not
- // structurally comparable, so we convert to array to compare. The conversion can be
- // removed once https://github.com/jsdom/jsdom/issues/2524 is resolved.
const encoder = new TextEncoder();
const uint8array = encoder.encode.bind(encoder);
expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"
- expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?')));
- expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~')));
- expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?')));
- expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~')));
+ expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?'));
+ expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~'));
+ expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?'));
+ expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~'));
expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
- expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a')));
- expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a')));
+ expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a'));
+ expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a'));
});
test('file detection', () => {
diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts
index 2d40fa20a8..c52bf500d4 100644
--- a/web_src/js/utils.ts
+++ b/web_src/js/utils.ts
@@ -1,13 +1,14 @@
import {encode, decode} from 'uint8-to-base64';
+import type {IssueData} from './types.ts';
// transform /path/to/file.ext to file.ext
-export function basename(path) {
+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
-export function extname(path) {
+export function extname(path: string): string {
const lastSlashIndex = path.lastIndexOf('/');
const lastPointIndex = path.lastIndexOf('.');
if (lastSlashIndex > lastPointIndex) return '';
@@ -15,54 +16,54 @@ export function extname(path) {
}
// test whether a variable is an object
-export function isObject(obj) {
+export function isObject(obj: any): boolean {
return Object.prototype.toString.call(obj) === '[object Object]';
}
// returns whether a dark theme is enabled
-export function isDarkTheme() {
+export function isDarkTheme(): boolean {
const style = window.getComputedStyle(document.documentElement);
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
}
// strip <tags> from a string
-export function stripTags(text) {
+export function stripTags(text: string): string {
return text.replace(/<[^>]*>?/g, '');
}
-export function parseIssueHref(href) {
+export function parseIssueHref(href: string): IssueData {
const path = (href || '').replace(/[#?].*$/, '');
const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
return {owner, repo, type, index};
}
// parse a URL, either relative '/path' or absolute 'https://localhost/path'
-export function parseUrl(str) {
+export function parseUrl(str: string): URL {
return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
}
// return current locale chosen by user
-export function getCurrentLocale() {
+export function getCurrentLocale(): string {
return document.documentElement.lang;
}
// given a month (0-11), returns it in the documents language
-export function translateMonth(month) {
+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
-export function translateDay(day) {
+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
-export function blobToDataURI(blob) {
+export function blobToDataURI(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader();
reader.addEventListener('load', (e) => {
- resolve(e.target.result);
+ resolve(e.target.result as string);
});
reader.addEventListener('error', () => {
reject(new Error('FileReader failed'));
@@ -75,7 +76,7 @@ export function blobToDataURI(blob) {
}
// convert image Blob to another mime-type format.
-export function convertImage(blob, mime) {
+export function convertImage(blob: Blob, mime: string): Promise<Blob> {
return new Promise(async (resolve, reject) => {
try {
const img = new Image();
@@ -104,7 +105,7 @@ export function convertImage(blob, mime) {
});
}
-export function toAbsoluteUrl(url) {
+export function toAbsoluteUrl(url: string): string {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
@@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) {
}
// Encode an ArrayBuffer into a URLEncoded base64 string.
-export function encodeURLEncodedBase64(arrayBuffer) {
+export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string {
return encode(arrayBuffer)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
-// Decode a URLEncoded base64 to an ArrayBuffer string.
-export function decodeURLEncodedBase64(base64url) {
+// Decode a URLEncoded base64 to an ArrayBuffer.
+export function decodeURLEncodedBase64(base64url: string): ArrayBuffer {
return decode(base64url
.replace(/_/g, '/')
.replace(/-/g, '+'));
@@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) {
const domParser = new DOMParser();
const xmlSerializer = new XMLSerializer();
-export function parseDom(text, contentType) {
+export function parseDom(text: string, contentType: DOMParserSupportedType): Document {
return domParser.parseFromString(text, contentType);
}
-export function serializeXml(node) {
+export function serializeXml(node: Element | Node): string {
return xmlSerializer.serializeToString(node);
}
-export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+export function sleep(ms: number): Promise<void> {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
-export function isImageFile({name, type}) {
+export function isImageFile({name, type}: {name: string, type?: string}): boolean {
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
}
-export function isVideoFile({name, type}) {
+export function isVideoFile({name, type}: {name: string, type?: string}): boolean {
return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
}
diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts
index 3ee32395fb..a0409353d2 100644
--- a/web_src/js/utils/color.ts
+++ b/web_src/js/utils/color.ts
@@ -3,23 +3,23 @@ import type {ColorInput} from 'tinycolor2';
// 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) {
+function getRelativeLuminance(color: ColorInput): number {
const {r, g, b} = tinycolor(color).toRgb();
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
}
-function useLightText(backgroundColor: ColorInput) {
+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.
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
-export function contrastColor(backgroundColor: ColorInput) {
+export function contrastColor(backgroundColor: ColorInput): string {
return useLightText(backgroundColor) ? '#fff' : '#000';
}
-function resolveColors(obj: Record<string, string>) {
+function resolveColors(obj: Record<string, string>): Record<string, string> {
const styles = window.getComputedStyle(document.documentElement);
const getColor = (name: string) => styles.getPropertyValue(name).trim();
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 5fc2183194..7dd63ecbbf 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() {
/**
* Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
* Note: This function doesn't account for all possible visibility scenarios.
- * @param {HTMLElement} element The element to check.
- * @returns {boolean} True if the element is visible.
*/
-export function isElemVisible(element: HTMLElement) {
+export function isElemVisible(element: HTMLElement): boolean {
if (!element) return false;
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
diff --git a/web_src/js/utils/image.ts b/web_src/js/utils/image.ts
index c71d715941..558a63f22e 100644
--- a/web_src/js/utils/image.ts
+++ b/web_src/js/utils/image.ts
@@ -1,6 +1,11 @@
-export async function pngChunks(blob) {
+type PngChunk = {
+ name: string,
+ data: Uint8Array,
+}
+
+export async function pngChunks(blob: Blob): Promise<PngChunk[]> {
const uint8arr = new Uint8Array(await blob.arrayBuffer());
- const chunks = [];
+ const chunks: PngChunk[] = [];
if (uint8arr.length < 12) return chunks;
const view = new DataView(uint8arr.buffer);
if (view.getBigUint64(0) !== 9894494448401390090n) return chunks;
@@ -19,9 +24,14 @@ export async function pngChunks(blob) {
return chunks;
}
+type ImageInfo = {
+ width?: number,
+ dppx?: number,
+}
+
// decode a image and try to obtain width and dppx. It will never throw but instead
// return default values.
-export async function imageInfo(blob) {
+export async function imageInfo(blob: Blob): Promise<ImageInfo> {
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens
if (blob.type === 'image/png') { // only png is supported currently
diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts
index 17fdfed113..0ce7e2b1a2 100644
--- a/web_src/js/utils/match.ts
+++ b/web_src/js/utils/match.ts
@@ -2,17 +2,17 @@ import emojis from '../../../assets/emoji.json';
const maxMatches = 6;
-function sortAndReduce(map) {
+function sortAndReduce(map: Map<string, number>) {
const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
return Array.from(sortedMap.keys()).slice(0, maxMatches);
}
-export function matchEmoji(queryText) {
+export function matchEmoji(queryText: string): string[] {
const query = queryText.toLowerCase().replaceAll('_', ' ');
if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
// results is a map of weights, lower is better
- const results = new Map();
+ const results = new Map<string, number>();
for (const {aliases} of emojis) {
const mainAlias = aliases[0];
for (const [aliasIndex, alias] of aliases.entries()) {
@@ -27,7 +27,7 @@ export function matchEmoji(queryText) {
return sortAndReduce(results);
}
-export function matchMention(queryText) {
+export function matchMention(queryText: string): string[] {
const query = queryText.toLowerCase();
// results is a map of weights, lower is better
diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts
index d3a986e736..5251386230 100644
--- a/web_src/js/utils/time.ts
+++ b/web_src/js/utils/time.ts
@@ -1,16 +1,17 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import {getCurrentLocale} from '../utils.ts';
+import type {ConfigType} from 'dayjs';
dayjs.extend(utc);
/**
* Returns an array of millisecond-timestamps of start-of-week days (Sundays)
*
- * @param startConfig The start date. Can take any type that `Date` accepts.
- * @param endConfig The end date. Can take any type that `Date` accepts.
+ * @param startDate The start date. Can take any type that dayjs accepts.
+ * @param endDate The end date. Can take any type that dayjs accepts.
*/
-export function startDaysBetween(startDate, endDate) {
+export function startDaysBetween(startDate: ConfigType, endDate: ConfigType): number[] {
const start = dayjs.utc(startDate);
const end = dayjs.utc(endDate);
@@ -21,7 +22,7 @@ export function startDaysBetween(startDate, endDate) {
current = current.add(1, 'day');
}
- const startDays = [];
+ const startDays: number[] = [];
while (current.isBefore(end)) {
startDays.push(current.valueOf());
current = current.add(1, 'week');
@@ -30,7 +31,7 @@ export function startDaysBetween(startDate, endDate) {
return startDays;
}
-export function firstStartDateAfterDate(inputDate) {
+export function firstStartDateAfterDate(inputDate: Date): number {
if (!(inputDate instanceof Date)) {
throw new Error('Invalid date');
}
@@ -41,7 +42,14 @@ export function firstStartDateAfterDate(inputDate) {
return resultDate.valueOf();
}
-export function fillEmptyStartDaysWithZeroes(startDays, data) {
+type DayData = {
+ week: number,
+ additions: number,
+ deletions: number,
+ commits: number,
+}
+
+export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] {
const result = {};
for (const startDay of startDays) {
@@ -51,11 +59,11 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) {
return Object.values(result);
}
-let dateFormat;
+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.
-export function formatDatetime(date) {
+export function formatDatetime(date: Date | number): string {
if (!dateFormat) {
// TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts
index 470ece31b0..c5a28774a9 100644
--- a/web_src/js/utils/url.ts
+++ b/web_src/js/utils/url.ts
@@ -1,12 +1,12 @@
-export function pathEscapeSegments(s) {
+export function pathEscapeSegments(s: string): string {
return s.split('/').map(encodeURIComponent).join('/');
}
-function stripSlash(url) {
+function stripSlash(url: string): string {
return url.endsWith('/') ? url.slice(0, -1) : url;
}
-export function isUrl(url) {
+export function isUrl(url: string): boolean {
try {
return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim();
} catch {
diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts
index 6fb0f5dc8f..68e300f551 100644
--- a/web_src/js/vitest.setup.ts
+++ b/web_src/js/vitest.setup.ts
@@ -1,10 +1,16 @@
window.__webpack_public_path__ = '';
window.config = {
+ appUrl: 'http://localhost:3000/',
+ appSubUrl: '',
+ assetVersionEncoded: '',
+ assetUrlPrefix: '',
+ runModeIsProd: true,
+ customEmojis: {},
csrfToken: 'test-csrf-token-123456',
pageData: {},
- i18n: {},
- appSubUrl: '',
+ notificationSettings: {},
+ enableTimeTracking: true,
mentionValues: [
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
{key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
@@ -14,4 +20,6 @@ window.config = {
{key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'},
{key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'},
],
+ mermaidMaxSourceCharacters: 5000,
+ i18n: {},
};