aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorsilverwind <me@silverwind.io>2023-06-27 04:45:24 +0200
committerGitHub <noreply@github.com>2023-06-27 02:45:24 +0000
commitc71e8abbc331e2a68186aa11a4797ecd24ff6d27 (patch)
tree24488621201c7501bd6056053e9cca3de221fce1 /web_src/js
parent72c60f94c1a54707ebad2af0f587c08d2e8f992c (diff)
downloadgitea-c71e8abbc331e2a68186aa11a4797ecd24ff6d27.tar.gz
gitea-c71e8abbc331e2a68186aa11a4797ecd24ff6d27.zip
Add toasts to UI (#25449)
Fixes https://github.com/go-gitea/gitea/issues/24353 In some case like async success/error, it is useful to show toasts in UI.
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/features/common-global.js3
-rw-r--r--web_src/js/features/comp/ComboMarkdownEditor.js3
-rw-r--r--web_src/js/features/repo-issue-content.js5
-rw-r--r--web_src/js/features/repo-issue-list.js3
-rw-r--r--web_src/js/modules/toast.js60
-rw-r--r--web_src/js/modules/toast.test.js17
-rw-r--r--web_src/js/standalone/devtest.js11
7 files changed, 97 insertions, 5 deletions
diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index e5fd7c29fc..a99b29141d 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -9,6 +9,7 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
import {createTippy} from '../modules/tippy.js';
import {confirmModal} from './comp/ConfirmModal.js';
+import {showErrorToast} from '../modules/toast.js';
const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
@@ -439,7 +440,7 @@ export function initGlobalButtons() {
return;
}
// should never happen, otherwise there is a bug in code
- alert('Nothing to hide');
+ showErrorToast('Nothing to hide');
});
initGlobalShowModal();
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 103e71daae..3d696be75b 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -8,6 +8,7 @@ import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
import {renderPreviewPanelContent} from '../repo-editor.js';
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
import {initTextExpander} from './TextExpander.js';
+import {showErrorToast} from '../../modules/toast.js';
let elementIdCounter = 0;
@@ -26,7 +27,7 @@ export function validateTextareaNonEmpty($textarea) {
$form[0]?.reportValidity();
} else {
// The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
- alert('Require non-empty content');
+ showErrorToast('Require non-empty content');
}
return false;
}
diff --git a/web_src/js/features/repo-issue-content.js b/web_src/js/features/repo-issue-content.js
index d66f6ad4a4..fc916aea19 100644
--- a/web_src/js/features/repo-issue-content.js
+++ b/web_src/js/features/repo-issue-content.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import {svg} from '../svg.js';
+import {showErrorToast} from '../modules/toast.js';
const {appSubUrl, csrfToken} = window.config;
let i18nTextEdited;
@@ -39,12 +40,12 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
if (resp.ok) {
$dialog.modal('hide');
} else {
- alert(resp.message);
+ showErrorToast(resp.message);
}
});
}
} else { // required by eslint
- window.alert(`unknown option item: ${optionItem}`);
+ showErrorToast(`unknown option item: ${optionItem}`);
}
},
onHide() {
diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js
index 4d61de0ce5..5402f958f2 100644
--- a/web_src/js/features/repo-issue-list.js
+++ b/web_src/js/features/repo-issue-list.js
@@ -4,6 +4,7 @@ import {toggleElem} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat';
import {Sortable} from 'sortablejs';
import {confirmModal} from './comp/ConfirmModal.js';
+import {showErrorToast} from '../modules/toast.js';
function initRepoIssueListCheckboxes() {
const $issueSelectAll = $('.issue-checkbox-all');
@@ -75,7 +76,7 @@ function initRepoIssueListCheckboxes() {
).then(() => {
window.location.reload();
}).catch((reason) => {
- window.alert(reason.responseJSON.error);
+ showErrorToast(reason.responseJSON.error);
});
});
}
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
new file mode 100644
index 0000000000..b0d02dc644
--- /dev/null
+++ b/web_src/js/modules/toast.js
@@ -0,0 +1,60 @@
+import {htmlEscape} from 'escape-goat';
+import {svg} from '../svg.js';
+
+const levels = {
+ info: {
+ icon: 'octicon-check',
+ background: 'var(--color-green)',
+ duration: 2500,
+ },
+ warning: {
+ icon: 'gitea-exclamation',
+ background: 'var(--color-orange)',
+ duration: -1, // requires dismissal to hide
+ },
+ error: {
+ icon: 'gitea-exclamation',
+ background: 'var(--color-red)',
+ duration: -1, // requires dismissal to hide
+ },
+};
+
+// See https://github.com/apvarun/toastify-js#api for options
+async function showToast(message, level, {gravity, position, duration, ...other} = {}) {
+ if (!message) return;
+
+ const {default: Toastify} = await import(/* webpackChunkName: 'toastify' */'toastify-js');
+ const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
+
+ const toast = Toastify({
+ text: `
+ <div class='toast-icon'>${svg(icon)}</div>
+ <div class='toast-body'>${htmlEscape(message)}</div>
+ <button class='toast-close'>${svg('octicon-x')}</button>
+ `,
+ escapeMarkup: false,
+ gravity: gravity ?? 'top',
+ position: position ?? 'center',
+ duration: duration ?? levelDuration,
+ style: {background},
+ ...other,
+ });
+
+ toast.showToast();
+
+ toast.toastElement.querySelector('.toast-close').addEventListener('click', () => {
+ toast.removeElement(toast.toastElement);
+ });
+}
+
+export async function showInfoToast(message, opts) {
+ return await showToast(message, 'info', opts);
+}
+
+export async function showWarningToast(message, opts) {
+ return await showToast(message, 'warning', opts);
+}
+
+export async function showErrorToast(message, opts) {
+ return await showToast(message, 'error', opts);
+}
diff --git a/web_src/js/modules/toast.test.js b/web_src/js/modules/toast.test.js
new file mode 100644
index 0000000000..b691aaebb6
--- /dev/null
+++ b/web_src/js/modules/toast.test.js
@@ -0,0 +1,17 @@
+import {test, expect} from 'vitest';
+import {showInfoToast, showErrorToast, showWarningToast} from './toast.js';
+
+test('showInfoToast', async () => {
+ await showInfoToast('success 😀', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
+
+test('showWarningToast', async () => {
+ await showWarningToast('warning 😐', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
+
+test('showErrorToast', async () => {
+ await showErrorToast('error 🙁', {duration: -1});
+ expect(document.querySelector('.toastify')).toBeTruthy();
+});
diff --git a/web_src/js/standalone/devtest.js b/web_src/js/standalone/devtest.js
new file mode 100644
index 0000000000..d0ca511c0f
--- /dev/null
+++ b/web_src/js/standalone/devtest.js
@@ -0,0 +1,11 @@
+import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';
+
+document.getElementById('info-toast').addEventListener('click', () => {
+ showInfoToast('success 😀');
+});
+document.getElementById('warning-toast').addEventListener('click', () => {
+ showWarningToast('warning 😐');
+});
+document.getElementById('error-toast').addEventListener('click', () => {
+ showErrorToast('error 🙁');
+});