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
Show file tree
Hide file tree
Changes from 7 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
Expand Up @@ -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' {
Expand Down
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
];
Expand All @@ -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;
Expand All @@ -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 ?? {};
Expand Down Expand Up @@ -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)};
silverwind marked this conversation as resolved.
Show resolved Hide resolved
}

initGlobalErrorHandler();
16 changes: 9 additions & 7 deletions web_src/js/modules/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {isObject} from '../utils.ts';
import type {RequestData, RequestOpts} from '../types.ts';

const {csrfToken} = window.config;

Expand All @@ -8,8 +9,9 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
// 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)) {
Expand All @@ -34,8 +36,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
Expand Up @@ -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 in Intent]: {
icon: SvgName,
background: string,
duration: number,
}
}

const levels: ToastLevels = {
info: {
icon: 'octicon-check',
background: 'var(--color-green)',
Expand All @@ -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}`;

Expand Down Expand Up @@ -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);
}
10 changes: 6 additions & 4 deletions web_src/js/svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,19 @@ 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.

// retrieve an HTML string for given SVG icon name, size and additional classes
export function svg(name, size = 16, className = '') {
export function svg(name: SvgName, size = 16, className = '') {
if (!(name in svgs)) throw new Error(`Unknown SVG icon: ${name}`);
if (size === 16 && !className) return svgs[name];

const document = parseDom(svgs[name], 'image/svg+xml');
const svgNode = document.firstChild;
const svgNode = document.firstChild as SVGElement;
if (size !== 16) {
svgNode.setAttribute('width', String(size));
svgNode.setAttribute('height', String(size));
Expand All @@ -165,7 +167,7 @@ export function svg(name, size = 16, className = '') {
return serializeXml(svgNode);
}

export function svgParseOuterInner(name) {
export function svgParseOuterInner(name: SvgName) {
const svgStr = svgs[name];
if (!svgStr) throw new Error(`Unknown SVG icon: ${name}`);

Expand All @@ -179,7 +181,7 @@ export function svgParseOuterInner(name) {
const svgInnerHtml = svgStr.slice(p1 + 1, p2);
const svgOuterHtml = svgStr.slice(0, p1 + 1) + svgStr.slice(p2);
const svgDoc = parseDom(svgOuterHtml, 'image/svg+xml');
const svgOuter = svgDoc.firstChild;
const svgOuter = svgDoc.firstChild as SVGElement;
return {svgOuter, svgInnerHtml};
}

Expand Down
8 changes: 8 additions & 0 deletions web_src/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ export type Config = {
mermaidMaxSourceCharacters: number,
i18n: Record<string, string>,
}

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

export type RequestData = string | FormData | URLSearchParams;

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