Skip to content
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

Add types to fetch,toast,bootstrap,svg #31627

Merged
merged 9 commits into from
Jul 25, 2024
Merged
Changes from 3 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
4 changes: 4 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@ interface Window {
$: 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' {
25 changes: 8 additions & 17 deletions web_src/js/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// DO NOT IMPORT window.config HERE!
// to make sure the error handler always works, we should never import `window.config`, because
// some user's custom template breaks it.
import type {Intent} from './types.ts';

// This sets up the URL prefix used in webpack's chunk loading.
// This file must be imported before any lazy-loading is being attempted.
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;

function shouldIgnoreError(err) {
function shouldIgnoreError(err: Error) {
const ignorePatterns = [
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496
];
@@ -16,14 +17,14 @@ function shouldIgnoreError(err) {
return false;
}

export function showGlobalErrorMessage(msg, msgType = 'error') {
export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') {
const msgContainer = document.querySelector('.page-content') ?? document.body;
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
if (!msgDiv) {
const el = document.createElement('div');
el.innerHTML = `<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`;
msgDiv = el.childNodes[0];
msgDiv = el.childNodes[0] as HTMLDivElement;
}
// merge duplicated messages into "the message (count)" format
const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1;
@@ -33,18 +34,7 @@ export function showGlobalErrorMessage(msg, msgType = 'error') {
msgContainer.prepend(msgDiv);
}

/**
* @param {ErrorEvent|PromiseRejectionEvent} event - Event
* @param {string} event.message - Only present on ErrorEvent
* @param {string} event.error - Only present on ErrorEvent
* @param {string} event.type - Only present on ErrorEvent
* @param {string} event.filename - Only present on ErrorEvent
* @param {number} event.lineno - Only present on ErrorEvent
* @param {number} event.colno - Only present on ErrorEvent
* @param {string} event.reason - Only present on PromiseRejectionEvent
* @param {number} event.promise - Only present on PromiseRejectionEvent
*/
function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}) {
function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
const err = error ?? reason;
const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
const {runModeIsProd} = window.config ?? {};
@@ -90,7 +80,8 @@ function initGlobalErrorHandler() {
}
// then, change _globalHandlerErrors to an object with push method, to process further error
// events directly
window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
// @ts-expect-error -- this should be refactored to not use a fake array
window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)};
}

initGlobalErrorHandler();
21 changes: 14 additions & 7 deletions web_src/js/modules/fetch.ts
Original file line number Diff line number Diff line change
@@ -5,11 +5,18 @@ const {csrfToken} = window.config;
// safe HTTP methods that don't need a csrf token
const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);

type RequestData = string | FormData | URLSearchParams;

type RequestOpts = {
data?: RequestData,
} & RequestInit;

// fetch wrapper, use below method name functions and the `data` option to pass in data
// which will automatically set an appropriate headers. For json content, only object
// and array types are currently supported.
export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) {
let body, contentType;
export function request(url: string, {method = 'GET', data, headers = {}, ...other}: RequestOpts = {}) {
let body: RequestData;
let contentType: string;
if (data instanceof FormData || data instanceof URLSearchParams) {
body = data;
} else if (isObject(data) || Array.isArray(data)) {
@@ -34,8 +41,8 @@ export function request(url, {method = 'GET', data, headers = {}, ...other} = {}
});
}

export const GET = (url, opts) => request(url, {method: 'GET', ...opts});
export const POST = (url, opts) => request(url, {method: 'POST', ...opts});
export const PATCH = (url, opts) => request(url, {method: 'PATCH', ...opts});
export const PUT = (url, opts) => request(url, {method: 'PUT', ...opts});
export const DELETE = (url, opts) => request(url, {method: 'DELETE', ...opts});
export const GET = (url: string, opts?: RequestOpts) => request(url, {method: 'GET', ...opts});
export const POST = (url: string, opts?: RequestOpts) => request(url, {method: 'POST', ...opts});
export const PATCH = (url: string, opts?: RequestOpts) => request(url, {method: 'PATCH', ...opts});
export const PUT = (url: string, opts?: RequestOpts) => request(url, {method: 'PUT', ...opts});
export const DELETE = (url: string, opts?: RequestOpts) => request(url, {method: 'DELETE', ...opts});
26 changes: 21 additions & 5 deletions web_src/js/modules/toast.ts
Original file line number Diff line number Diff line change
@@ -2,8 +2,19 @@ import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.ts';
import {animateOnce, showElem} from '../utils/dom.ts';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
import type {Intent} from '../types.ts';
import type {SvgName} from '../svg.ts';
import type {Options} from 'toastify-js';

const levels = {
type ToastLevels = {
[intent: string]: {
icon: SvgName,
background: string,
duration: number,
}
}

const levels: ToastLevels = {
info: {
icon: 'octicon-check',
background: 'var(--color-green)',
@@ -21,8 +32,13 @@ const levels = {
},
};

type ToastOpts = {
useHtmlBody?: boolean,
preventDuplicates?: boolean,
} & Options;

// See https://github.com/apvarun/toastify-js#api for options
function showToast(message, level, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other} = {}) {
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}) {
const body = useHtmlBody ? String(message) : htmlEscape(message);
const key = `${level}-${body}`;

@@ -59,14 +75,14 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, pr
return toast;
}

export function showInfoToast(message, opts) {
export function showInfoToast(message: string, opts?: ToastOpts) {
return showToast(message, 'info', opts);
}

export function showWarningToast(message, opts) {
export function showWarningToast(message: string, opts?: ToastOpts) {
return showToast(message, 'warning', opts);
}

export function showErrorToast(message, opts) {
export function showErrorToast(message: string, opts?: ToastOpts) {
return showToast(message, 'error', opts);
}
2 changes: 2 additions & 0 deletions web_src/js/svg.ts
Original file line number Diff line number Diff line change
@@ -146,6 +146,8 @@ const svgs = {
'octicon-x-circle-fill': octiconXCircleFill,
};

export type SvgName = keyof typeof svgs;

// TODO: use a more general approach to access SVG icons.
// At the moment, developers must check, pick and fill the names manually,
// most of the SVG icons in assets couldn't be used directly.
2 changes: 2 additions & 0 deletions web_src/js/types.ts
Original file line number Diff line number Diff line change
@@ -21,3 +21,5 @@ export type Config = {
mermaidMaxSourceCharacters: number,
i18n: Record<string, string>,
}

export type Intent = 'error' | 'warning' | 'info';