mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
							
								
								
									
										52
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										52
									
								
								types.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -10,22 +10,52 @@ declare module '*.css' { | |||||||
|  |  | ||||||
| declare let __webpack_public_path__: string; | declare let __webpack_public_path__: string; | ||||||
|  |  | ||||||
| interface Window { |  | ||||||
|   config: import('./web_src/js/types.ts').Config; |  | ||||||
|   $: typeof import('@types/jquery'), |  | ||||||
|   jQuery: typeof import('@types/jquery'), |  | ||||||
|   htmx: typeof import('htmx.org'), |  | ||||||
|   _globalHandlerErrors: Array<ErrorEvent & PromiseRejectionEvent> & { |  | ||||||
|     _inited: boolean, |  | ||||||
|     push: (e: ErrorEvent & PromiseRejectionEvent) => void | number, |  | ||||||
|   }, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare module 'htmx.org/dist/htmx.esm.js' { | declare module 'htmx.org/dist/htmx.esm.js' { | ||||||
|   const value = await import('htmx.org'); |   const value = await import('htmx.org'); | ||||||
|   export default value; |   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 { | interface Element { | ||||||
|   _tippy: import('tippy.js').Instance; |   _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: 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, | ||||||
|  |   }, | ||||||
|  |   __webpack_public_path__: string; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,20 +1,21 @@ | |||||||
| import {showErrorToast} from './modules/toast.ts'; | 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 | type HtmxEvent = Event & {detail: HtmxResponseInfo}; | ||||||
| import 'idiomorph/dist/idiomorph-ext.js'; |  | ||||||
|  |  | ||||||
| // https://htmx.org/reference/#config | // https://htmx.org/reference/#config | ||||||
| window.htmx.config.requestClass = 'is-loading'; | window.htmx.config.requestClass = 'is-loading'; | ||||||
| window.htmx.config.scrollIntoViewOnBoost = false; | window.htmx.config.scrollIntoViewOnBoost = false; | ||||||
|  |  | ||||||
| // https://htmx.org/events/#htmx:sendError | // https://htmx.org/events/#htmx:sendError | ||||||
| document.body.addEventListener('htmx:sendError', (event) => { | document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => { | ||||||
|   // TODO: add translations |   // TODO: add translations | ||||||
|   showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); |   showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // https://htmx.org/events/#htmx:responseError | // https://htmx.org/events/#htmx:responseError | ||||||
| document.body.addEventListener('htmx:responseError', (event) => { | document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => { | ||||||
|   // TODO: add translations |   // TODO: add translations | ||||||
|   showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); |   showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -98,12 +98,12 @@ initGiteaFomantic(); | |||||||
| initDirAuto(); | initDirAuto(); | ||||||
| initSubmitEventPolyfill(); | 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" |   // 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. |   // It is a quick check, no side effect so no need to do slow URL parsing. | ||||||
|   const initStart = performance.now(); |   const initStart = performance.now(); | ||||||
|   if (window.location.search.includes('_ui_performance_trace=1')) { |   if (window.location.search.includes('_ui_performance_trace=1')) { | ||||||
|     let results = []; |     let results: {name: string, dur: number}[] = []; | ||||||
|     for (const func of functions) { |     for (const func of functions) { | ||||||
|       const start = performance.now(); |       const start = performance.now(); | ||||||
|       func(); |       func(); | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import {AnsiUp} from 'ansi_up'; | 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+[A-H]/g, ''], // Move cursor, treat them as no-op | ||||||
|   [/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return |   [/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| // render ANSI to HTML | // 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 |   // 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 |   // the output of future renders, because ansi_up is stateful and remembers things like | ||||||
|   // unclosed opening tags for colors. |   // unclosed opening tags for colors. | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ window.addEventListener('load', async () => { | |||||||
|  |  | ||||||
|   // Make the page's protocol be at the top of the schemes list |   // Make the page's protocol be at the top of the schemes list | ||||||
|   const proto = window.location.protocol.slice(0, -1); |   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 (a === proto) return -1; | ||||||
|     if (b === proto) return 1; |     if (b === proto) return 1; | ||||||
|     return 0; |     return 0; | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ test('svgParseOuterInner', () => { | |||||||
| test('SvgIcon', () => { | test('SvgIcon', () => { | ||||||
|   const root = document.createElement('div'); |   const root = document.createElement('div'); | ||||||
|   createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root); |   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.nodeName).toEqual('svg'); | ||||||
|   expect(node.getAttribute('width')).toEqual('24'); |   expect(node.getAttribute('width')).toEqual('24'); | ||||||
|   expect(node.getAttribute('height')).toEqual('24'); |   expect(node.getAttribute('height')).toEqual('24'); | ||||||
|   | |||||||
| @@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams; | |||||||
| export type RequestOpts = { | export type RequestOpts = { | ||||||
|   data?: RequestData, |   data?: RequestData, | ||||||
| } & RequestInit; | } & RequestInit; | ||||||
|  |  | ||||||
|  | export type IssueData = { | ||||||
|  |   owner: string, | ||||||
|  |   repo: string, | ||||||
|  |   type: string, | ||||||
|  |   index: string, | ||||||
|  | } | ||||||
|   | |||||||
| @@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { | 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 encoder = new TextEncoder(); | ||||||
|   const uint8array = encoder.encode.bind(encoder); |   const uint8array = encoder.encode.bind(encoder); | ||||||
|  |  | ||||||
|   expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" |   expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" | ||||||
|   expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" |   expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" | ||||||
|  |  | ||||||
|   expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?'))); |   expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?')); | ||||||
|   expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~'))); |   expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~')); | ||||||
|   expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?'))); |   expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?')); | ||||||
|   expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~'))); |   expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~')); | ||||||
|  |  | ||||||
|   expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ==" |   expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ==" | ||||||
|   expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a'))); |   expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a')); | ||||||
|   expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a'))); |   expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a')); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test('file detection', () => { | test('file detection', () => { | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| import {encode, decode} from 'uint8-to-base64'; | import {encode, decode} from 'uint8-to-base64'; | ||||||
|  | import type {IssueData} from './types.ts'; | ||||||
|  |  | ||||||
| // transform /path/to/file.ext to file.ext | // transform /path/to/file.ext to file.ext | ||||||
| export function basename(path) { | export function basename(path: string): string { | ||||||
|   const lastSlashIndex = path.lastIndexOf('/'); |   const lastSlashIndex = path.lastIndexOf('/'); | ||||||
|   return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1); |   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) { | export function extname(path: string): string { | ||||||
|   const lastSlashIndex = path.lastIndexOf('/'); |   const lastSlashIndex = path.lastIndexOf('/'); | ||||||
|   const lastPointIndex = path.lastIndexOf('.'); |   const lastPointIndex = path.lastIndexOf('.'); | ||||||
|   if (lastSlashIndex > lastPointIndex) return ''; |   if (lastSlashIndex > lastPointIndex) return ''; | ||||||
| @@ -15,54 +16,54 @@ export function extname(path) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // test whether a variable is an object | // 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]'; |   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() { | export function isDarkTheme(): boolean { | ||||||
|   const style = window.getComputedStyle(document.documentElement); |   const style = window.getComputedStyle(document.documentElement); | ||||||
|   return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true'; |   return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true'; | ||||||
| } | } | ||||||
|  |  | ||||||
| // strip <tags> from a string | // strip <tags> from a string | ||||||
| export function stripTags(text) { | export function stripTags(text: string): string { | ||||||
|   return text.replace(/<[^>]*>?/g, ''); |   return text.replace(/<[^>]*>?/g, ''); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function parseIssueHref(href) { | export function parseIssueHref(href: string): IssueData { | ||||||
|   const path = (href || '').replace(/[#?].*$/, ''); |   const path = (href || '').replace(/[#?].*$/, ''); | ||||||
|   const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; |   const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; | ||||||
|   return {owner, repo, type, index}; |   return {owner, repo, type, index}; | ||||||
| } | } | ||||||
|  |  | ||||||
| // 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) { | export function parseUrl(str: string): URL { | ||||||
|   return new URL(str, str.startsWith('http') ? undefined : window.location.origin); |   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() { | export function getCurrentLocale(): string { | ||||||
|   return document.documentElement.lang; |   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) { | export function translateMonth(month: number) { | ||||||
|   return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'}); |   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) { | export function translateDay(day: number) { | ||||||
|   return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'}); |   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) { | export function blobToDataURI(blob: Blob): Promise<string> { | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     try { |     try { | ||||||
|       const reader = new FileReader(); |       const reader = new FileReader(); | ||||||
|       reader.addEventListener('load', (e) => { |       reader.addEventListener('load', (e) => { | ||||||
|         resolve(e.target.result); |         resolve(e.target.result as string); | ||||||
|       }); |       }); | ||||||
|       reader.addEventListener('error', () => { |       reader.addEventListener('error', () => { | ||||||
|         reject(new Error('FileReader failed')); |         reject(new Error('FileReader failed')); | ||||||
| @@ -75,7 +76,7 @@ export function blobToDataURI(blob) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // convert image Blob to another mime-type format. | // 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) => { |   return new Promise(async (resolve, reject) => { | ||||||
|     try { |     try { | ||||||
|       const img = new Image(); |       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://')) { |   if (url.startsWith('http://') || url.startsWith('https://')) { | ||||||
|     return url; |     return url; | ||||||
|   } |   } | ||||||
| @@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // Encode an ArrayBuffer into a URLEncoded base64 string. | // Encode an ArrayBuffer into a URLEncoded base64 string. | ||||||
| export function encodeURLEncodedBase64(arrayBuffer) { | export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string { | ||||||
|   return encode(arrayBuffer) |   return encode(arrayBuffer) | ||||||
|     .replace(/\+/g, '-') |     .replace(/\+/g, '-') | ||||||
|     .replace(/\//g, '_') |     .replace(/\//g, '_') | ||||||
|     .replace(/=/g, ''); |     .replace(/=/g, ''); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Decode a URLEncoded base64 to an ArrayBuffer string. | // Decode a URLEncoded base64 to an ArrayBuffer. | ||||||
| export function decodeURLEncodedBase64(base64url) { | export function decodeURLEncodedBase64(base64url: string): ArrayBuffer { | ||||||
|   return decode(base64url |   return decode(base64url | ||||||
|     .replace(/_/g, '/') |     .replace(/_/g, '/') | ||||||
|     .replace(/-/g, '+')); |     .replace(/-/g, '+')); | ||||||
| @@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) { | |||||||
| const domParser = new DOMParser(); | const domParser = new DOMParser(); | ||||||
| const xmlSerializer = new XMLSerializer(); | const xmlSerializer = new XMLSerializer(); | ||||||
|  |  | ||||||
| export function parseDom(text, contentType) { | export function parseDom(text: string, contentType: DOMParserSupportedType): Document { | ||||||
|   return domParser.parseFromString(text, contentType); |   return domParser.parseFromString(text, contentType); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function serializeXml(node) { | export function serializeXml(node: Element | Node): string { | ||||||
|   return xmlSerializer.serializeToString(node); |   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/'); |   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/'); |   return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,23 +3,23 @@ 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 | // 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(); |   const {r, g, b} = tinycolor(color).toRgb(); | ||||||
|   return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; |   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; |   return getRelativeLuminance(backgroundColor) < 0.453; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Given a background color, returns a black or white foreground color that the highest | // 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. | // 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 | // 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'; |   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 styles = window.getComputedStyle(document.documentElement); | ||||||
|   const getColor = (name: string) => styles.getPropertyValue(name).trim(); |   const getColor = (name: string) => styles.getPropertyValue(name).trim(); | ||||||
|   return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); |   return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); | ||||||
|   | |||||||
| @@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() { | |||||||
| /** | /** | ||||||
|  * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. |  * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. | ||||||
|  * Note: This function doesn't account for all possible visibility scenarios. |  * 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; |   if (!element) return false; | ||||||
|  |  | ||||||
|   return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); |   return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); | ||||||
|   | |||||||
| @@ -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 uint8arr = new Uint8Array(await blob.arrayBuffer()); | ||||||
|   const chunks = []; |   const chunks: PngChunk[] = []; | ||||||
|   if (uint8arr.length < 12) return chunks; |   if (uint8arr.length < 12) return chunks; | ||||||
|   const view = new DataView(uint8arr.buffer); |   const view = new DataView(uint8arr.buffer); | ||||||
|   if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; |   if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; | ||||||
| @@ -19,9 +24,14 @@ export async function pngChunks(blob) { | |||||||
|   return chunks; |   return chunks; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type ImageInfo = { | ||||||
|  |   width?: number, | ||||||
|  |   dppx?: number, | ||||||
|  | } | ||||||
|  |  | ||||||
| // decode a image and try to obtain width and dppx. It will never throw but instead | // decode a image and try to obtain width and dppx. It will never throw but instead | ||||||
| // return default values. | // 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 |   let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens | ||||||
|  |  | ||||||
|   if (blob.type === 'image/png') { // only png is supported currently |   if (blob.type === 'image/png') { // only png is supported currently | ||||||
|   | |||||||
| @@ -2,17 +2,17 @@ import emojis from '../../../assets/emoji.json'; | |||||||
|  |  | ||||||
| const maxMatches = 6; | 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])); |   const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1])); | ||||||
|   return Array.from(sortedMap.keys()).slice(0, maxMatches); |   return Array.from(sortedMap.keys()).slice(0, maxMatches); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function matchEmoji(queryText) { | export function matchEmoji(queryText: string): string[] { | ||||||
|   const query = queryText.toLowerCase().replaceAll('_', ' '); |   const query = queryText.toLowerCase().replaceAll('_', ' '); | ||||||
|   if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]); |   if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]); | ||||||
|  |  | ||||||
|   // results is a map of weights, lower is better |   // results is a map of weights, lower is better | ||||||
|   const results = new Map(); |   const results = new Map<string, number>(); | ||||||
|   for (const {aliases} of emojis) { |   for (const {aliases} of emojis) { | ||||||
|     const mainAlias = aliases[0]; |     const mainAlias = aliases[0]; | ||||||
|     for (const [aliasIndex, alias] of aliases.entries()) { |     for (const [aliasIndex, alias] of aliases.entries()) { | ||||||
| @@ -27,7 +27,7 @@ export function matchEmoji(queryText) { | |||||||
|   return sortAndReduce(results); |   return sortAndReduce(results); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function matchMention(queryText) { | export function matchMention(queryText: string): string[] { | ||||||
|   const query = queryText.toLowerCase(); |   const query = queryText.toLowerCase(); | ||||||
|  |  | ||||||
|   // results is a map of weights, lower is better |   // results is a map of weights, lower is better | ||||||
|   | |||||||
| @@ -1,16 +1,17 @@ | |||||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||||
| import utc from 'dayjs/plugin/utc.js'; | import utc from 'dayjs/plugin/utc.js'; | ||||||
| import {getCurrentLocale} from '../utils.ts'; | import {getCurrentLocale} from '../utils.ts'; | ||||||
|  | import type {ConfigType} from 'dayjs'; | ||||||
|  |  | ||||||
| dayjs.extend(utc); | dayjs.extend(utc); | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns an array of millisecond-timestamps of start-of-week days (Sundays) |  * 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 startDate The start date. Can take any type that dayjs accepts. | ||||||
|  * @param endConfig The end date. Can take any type that `Date` 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 start = dayjs.utc(startDate); | ||||||
|   const end = dayjs.utc(endDate); |   const end = dayjs.utc(endDate); | ||||||
|  |  | ||||||
| @@ -21,7 +22,7 @@ export function startDaysBetween(startDate, endDate) { | |||||||
|     current = current.add(1, 'day'); |     current = current.add(1, 'day'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const startDays = []; |   const startDays: number[] = []; | ||||||
|   while (current.isBefore(end)) { |   while (current.isBefore(end)) { | ||||||
|     startDays.push(current.valueOf()); |     startDays.push(current.valueOf()); | ||||||
|     current = current.add(1, 'week'); |     current = current.add(1, 'week'); | ||||||
| @@ -30,7 +31,7 @@ export function startDaysBetween(startDate, endDate) { | |||||||
|   return startDays; |   return startDays; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function firstStartDateAfterDate(inputDate) { | export function firstStartDateAfterDate(inputDate: Date): number { | ||||||
|   if (!(inputDate instanceof Date)) { |   if (!(inputDate instanceof Date)) { | ||||||
|     throw new Error('Invalid date'); |     throw new Error('Invalid date'); | ||||||
|   } |   } | ||||||
| @@ -41,7 +42,14 @@ export function firstStartDateAfterDate(inputDate) { | |||||||
|   return resultDate.valueOf(); |   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 = {}; |   const result = {}; | ||||||
|  |  | ||||||
|   for (const startDay of startDays) { |   for (const startDay of startDays) { | ||||||
| @@ -51,11 +59,11 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) { | |||||||
|   return Object.values(result); |   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 | // 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. | // 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) { |   if (!dateFormat) { | ||||||
|     // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support |     // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support | ||||||
|     dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), { |     dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), { | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| export function pathEscapeSegments(s) { | export function pathEscapeSegments(s: string): string { | ||||||
|   return s.split('/').map(encodeURIComponent).join('/'); |   return s.split('/').map(encodeURIComponent).join('/'); | ||||||
| } | } | ||||||
|  |  | ||||||
| function stripSlash(url) { | function stripSlash(url: string): string { | ||||||
|   return url.endsWith('/') ? url.slice(0, -1) : url; |   return url.endsWith('/') ? url.slice(0, -1) : url; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function isUrl(url) { | export function isUrl(url: string): boolean { | ||||||
|   try { |   try { | ||||||
|     return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim(); |     return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim(); | ||||||
|   } catch { |   } catch { | ||||||
|   | |||||||
| @@ -1,10 +1,16 @@ | |||||||
| window.__webpack_public_path__ = ''; | window.__webpack_public_path__ = ''; | ||||||
|  |  | ||||||
| window.config = { | window.config = { | ||||||
|  |   appUrl: 'http://localhost:3000/', | ||||||
|  |   appSubUrl: '', | ||||||
|  |   assetVersionEncoded: '', | ||||||
|  |   assetUrlPrefix: '', | ||||||
|  |   runModeIsProd: true, | ||||||
|  |   customEmojis: {}, | ||||||
|   csrfToken: 'test-csrf-token-123456', |   csrfToken: 'test-csrf-token-123456', | ||||||
|   pageData: {}, |   pageData: {}, | ||||||
|   i18n: {}, |   notificationSettings: {}, | ||||||
|   appSubUrl: '', |   enableTimeTracking: true, | ||||||
|   mentionValues: [ |   mentionValues: [ | ||||||
|     {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'}, |     {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'}, |     {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: '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'}, |     {key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'}, | ||||||
|   ], |   ], | ||||||
|  |   mermaidMaxSourceCharacters: 5000, | ||||||
|  |   i18n: {}, | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user