Skip to content

Fix a number of Typescript issues #31877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
9 changes: 5 additions & 4 deletions web_src/js/htmx.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
4 changes: 2 additions & 2 deletions web_src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions web_src/js/render/ansi.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/standalone/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/svg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
7 changes: 7 additions & 0 deletions web_src/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
15 changes: 6 additions & 9 deletions web_src/js/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
47 changes: 25 additions & 22 deletions web_src/js/utils.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
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 '';
return lastPointIndex < 0 ? '' : path.substring(lastPointIndex);
}

// 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'));
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
Expand All @@ -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, '+'));
Expand All @@ -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/');
}
8 changes: 4 additions & 4 deletions web_src/js/utils/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)]));
Expand Down
4 changes: 1 addition & 3 deletions web_src/js/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 13 additions & 3 deletions web_src/js/utils/image.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Loading