Skip to content

Commit fa7f631

Browse files
committed
improve link-action
1 parent ea34641 commit fa7f631

File tree

3 files changed

+55
-73
lines changed

3 files changed

+55
-73
lines changed

templates/base/head_script.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ If you are customizing Gitea, please do not change this file.
44
If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
55
*/}}
66
<script>
7+
{{/* before our JS code gets loaded, use arrays to store errors, then the arrays will be switched to our error handler later */}}
78
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
9+
window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
810
window.config = {
911
appUrl: '{{AppUrl}}',
1012
appSubUrl: '{{AppSubUrl}}',

web_src/js/bootstrap.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function showGlobalErrorMessage(msg) {
2020
* @param {ErrorEvent} e
2121
*/
2222
function processWindowErrorEvent(e) {
23+
if (e.type === 'unhandledrejection') {
24+
showGlobalErrorMessage(`JavaScript promise rejection: ${e.reason}. Open browser console to see more details.`);
25+
return;
26+
}
2327
if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
2428
// At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
2529
// If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
@@ -30,6 +34,10 @@ function processWindowErrorEvent(e) {
3034
}
3135

3236
function initGlobalErrorHandler() {
37+
if (window._globalHandlerErrors?._inited) {
38+
showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`);
39+
return;
40+
}
3341
if (!window.config) {
3442
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
3543
}
@@ -40,7 +48,7 @@ function initGlobalErrorHandler() {
4048
processWindowErrorEvent(e);
4149
}
4250
// then, change _globalHandlerErrors to an object with push method, to process further error events directly
43-
window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)};
51+
window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
4452
}
4553

4654
initGlobalErrorHandler();

web_src/js/features/common-global.js

Lines changed: 44 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
88
import {svg} from '../svg.js';
99
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
1010
import {htmlEscape} from 'escape-goat';
11-
import {createTippy, showTemporaryTooltip} from '../modules/tippy.js';
11+
import {showTemporaryTooltip} from '../modules/tippy.js';
1212
import {confirmModal} from './comp/ConfirmModal.js';
1313
import {showErrorToast} from '../modules/toast.js';
1414

@@ -64,9 +64,9 @@ export function initGlobalButtonClickOnEnter() {
6464
});
6565
}
6666

67-
// doRedirect does real redirection to bypass the browser's limitations of "location"
67+
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
6868
// more details are in the backend's fetch-redirect handler
69-
function doRedirect(redirect) {
69+
function fetchActionDoRedirect(redirect) {
7070
const form = document.createElement('form');
7171
const input = document.createElement('input');
7272
form.method = 'post';
@@ -79,6 +79,33 @@ function doRedirect(redirect) {
7979
form.submit();
8080
}
8181

82+
async function fetchActionDoRequest(actionElem, url, opt) {
83+
try {
84+
const resp = await fetch(url, opt);
85+
if (resp.status === 200) {
86+
let {redirect} = await resp.json();
87+
redirect = redirect || actionElem.getAttribute('data-redirect');
88+
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
89+
if (redirect) {
90+
fetchActionDoRedirect(redirect);
91+
} else {
92+
window.location.reload();
93+
}
94+
} else if (resp.status >= 400 && resp.status < 500) {
95+
const data = await resp.json();
96+
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
97+
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
98+
await showErrorToast(data.errorMessage || `server error: ${resp.status}`);
99+
} else {
100+
await showErrorToast(`server error: ${resp.status}`);
101+
}
102+
} catch (e) {
103+
console.error('error when doRequest', e);
104+
actionElem.classList.remove('is-loading', 'small-loading-icon');
105+
await showErrorToast(i18n.network_error);
106+
}
107+
}
108+
82109
async function formFetchAction(e) {
83110
if (!e.target.classList.contains('form-fetch-action')) return;
84111

@@ -115,50 +142,7 @@ async function formFetchAction(e) {
115142
reqOpt.body = formData;
116143
}
117144

118-
let errorTippy;
119-
const onError = (msg) => {
120-
formEl.classList.remove('is-loading', 'small-loading-icon');
121-
if (errorTippy) errorTippy.destroy();
122-
// TODO: use a better toast UI instead of the tippy. If the form height is large, the tippy position is not good
123-
errorTippy = createTippy(formEl, {
124-
content: msg,
125-
interactive: true,
126-
showOnCreate: true,
127-
hideOnClick: true,
128-
role: 'alert',
129-
theme: 'form-fetch-error',
130-
trigger: 'manual',
131-
arrow: false,
132-
});
133-
};
134-
135-
const doRequest = async () => {
136-
try {
137-
const resp = await fetch(reqUrl, reqOpt);
138-
if (resp.status === 200) {
139-
const {redirect} = await resp.json();
140-
formEl.classList.remove('dirty'); // remove the areYouSure check before reloading
141-
if (redirect) {
142-
doRedirect(redirect);
143-
} else {
144-
window.location.reload();
145-
}
146-
} else if (resp.status >= 400 && resp.status < 500) {
147-
const data = await resp.json();
148-
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
149-
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
150-
onError(data.errorMessage || `server error: ${resp.status}`);
151-
} else {
152-
onError(`server error: ${resp.status}`);
153-
}
154-
} catch (e) {
155-
console.error('error when doRequest', e);
156-
onError(i18n.network_error);
157-
}
158-
};
159-
160-
// TODO: add "confirm" support like "link-action" in the future
161-
await doRequest();
145+
await fetchActionDoRequest(formEl, reqUrl, reqOpt);
162146
}
163147

164148
export function initGlobalCommon() {
@@ -209,6 +193,7 @@ export function initGlobalCommon() {
209193
$('.tabular.menu .item').tab();
210194

211195
document.addEventListener('submit', formFetchAction);
196+
document.addEventListener('click', linkAction);
212197
}
213198

214199
export function initGlobalDropzone() {
@@ -269,41 +254,29 @@ export function initGlobalDropzone() {
269254
}
270255

271256
async function linkAction(e) {
272-
e.preventDefault();
273-
274257
// A "link-action" can post AJAX request to its "data-url"
275258
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
276259
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
260+
const el = e.target.closest('.link-action');
261+
if (!el) return;
277262

278-
const $this = $(this);
279-
const redirect = $this.attr('data-redirect');
280-
281-
const doRequest = () => {
282-
$this.prop('disabled', true);
283-
$.post($this.attr('data-url'), {
284-
_csrf: csrfToken
285-
}).done((data) => {
286-
if (data && data.redirect) {
287-
window.location.href = data.redirect;
288-
} else if (redirect) {
289-
window.location.href = redirect;
290-
} else {
291-
window.location.reload();
292-
}
293-
}).always(() => {
294-
$this.prop('disabled', false);
295-
});
263+
e.preventDefault();
264+
const url = el.getAttribute('data-url');
265+
const doRequest = async () => {
266+
el.disabled = true;
267+
await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}});
268+
el.disabled = false;
296269
};
297270

298-
const modalConfirmContent = htmlEscape($this.attr('data-modal-confirm') || '');
271+
const modalConfirmContent = htmlEscape(el.getAttribute('data-modal-confirm') || '');
299272
if (!modalConfirmContent) {
300-
doRequest();
273+
await doRequest();
301274
return;
302275
}
303276

304-
const isRisky = $this.hasClass('red') || $this.hasClass('yellow') || $this.hasClass('orange') || $this.hasClass('negative');
277+
const isRisky = el.classList.contains('red') || el.classList.contains('yellow') || el.classList.contains('orange') || el.classList.contains('negative');
305278
if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'green'})) {
306-
doRequest();
279+
await doRequest();
307280
}
308281
}
309282

@@ -354,7 +327,6 @@ export function initGlobalLinkActions() {
354327

355328
// Helpers.
356329
$('.delete-button').on('click', showDeletePopup);
357-
$('.link-action').on('click', linkAction);
358330
}
359331

360332
function initGlobalShowModal() {

0 commit comments

Comments
 (0)