loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

authored by

silverwind and committed by
GitHub
c71e8abb 72c60f94

+220 -20
+4 -3
.eslintrc.yaml
··· 25 25 es2022: true 26 26 node: true 27 27 28 - globals: 29 - __webpack_public_path__: true 30 - 31 28 overrides: 29 + - files: ["web_src/**/*"] 30 + globals: 31 + __webpack_public_path__: true 32 + process: false # https://github.com/webpack/webpack/issues/15833 32 33 - files: ["web_src/**/*", "docs/**/*"] 33 34 env: 34 35 browser: true
+6
package-lock.json
··· 41 41 "swagger-ui-dist": "5.0.0", 42 42 "throttle-debounce": "5.0.0", 43 43 "tippy.js": "6.3.7", 44 + "toastify-js": "1.12.0", 44 45 "tributejs": "5.1.3", 45 46 "uint8-to-base64": "0.2.0", 46 47 "vue": "3.3.4", ··· 10121 10122 "engines": { 10122 10123 "node": ">=8.0" 10123 10124 } 10125 + }, 10126 + "node_modules/toastify-js": { 10127 + "version": "1.12.0", 10128 + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", 10129 + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" 10124 10130 }, 10125 10131 "node_modules/toidentifier": { 10126 10132 "version": "1.0.1",
+1
package.json
··· 40 40 "swagger-ui-dist": "5.0.0", 41 41 "throttle-debounce": "5.0.0", 42 42 "tippy.js": "6.3.7", 43 + "toastify-js": "1.12.0", 43 44 "tributejs": "5.1.3", 44 45 "uint8-to-base64": "0.2.0", 45 46 "vue": "3.3.4",
+11 -12
templates/devtest/gitea-ui.tmpl
··· 1 1 {{template "base/head" .}} 2 + <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> 2 3 <div class="page-content devtest ui container"> 3 4 <div> 4 5 <h1>Button</h1> ··· 14 15 <label><input type="checkbox" name="button-state-disabled" value="disabled">disabled</label> 15 16 </div> 16 17 <div id="devtest-button-samples"> 17 - <style> 18 - .button-sample-groups { margin: 0; padding: 0; } 19 - .button-sample-groups .sample-group { list-style: none; margin: 0; padding: 0; } 20 - .button-sample-groups .sample-group .ui.button { margin-bottom: 5px; } 21 - </style> 22 18 <ul class="button-sample-groups"> 23 19 <li class="sample-group"> 24 20 <h2>General purpose:</h2> ··· 243 239 </div> 244 240 245 241 <div> 242 + <h1>Toast</h1> 243 + <div> 244 + <button class="ui button" id="info-toast">Show Info Toast</button> 245 + <button class="ui button" id="warning-toast">Show Warning Toast</button> 246 + <button class="ui button" id="error-toast">Show Error Toast</button> 247 + </div> 248 + </div> 249 + 250 + <div> 246 251 <h1>ComboMarkdownEditor</h1> 247 252 <div>ps: no JS code attached, so just a layout</div> 248 253 {{template "shared/combomarkdowneditor" .}} 249 254 </div> 250 - 251 - <style> 252 - h1, h2 { 253 - margin: 0; 254 - padding: 10px 0; 255 - } 256 - </style> 255 + <script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script> 257 256 </div> 258 257 {{template "base/footer" .}}
+1
web_src/css/index.css
··· 8 8 @import "./modules/card.css"; 9 9 @import "./modules/comment.css"; 10 10 @import "./modules/navbar.css"; 11 + @import "./modules/toast.css"; 11 12 12 13 @import "./shared/issuelist.css"; 13 14 @import "./shared/milestone.css";
+78
web_src/css/modules/toast.css
··· 1 + .toastify { 2 + color: var(--color-white); 3 + position: fixed; 4 + opacity: 0; 5 + transition: all .2s ease; 6 + z-index: 500; 7 + border-radius: 4px; 8 + box-shadow: 0 8px 24px var(--color-shadow); 9 + display: flex; 10 + max-width: 50vw; 11 + min-width: 300px; 12 + padding: 4px; 13 + } 14 + 15 + .toastify.on { 16 + opacity: 1; 17 + } 18 + 19 + .toast-body { 20 + flex: 1; 21 + padding: 5px 0; 22 + overflow-wrap: anywhere; 23 + } 24 + 25 + .toast-close, 26 + .toast-icon { 27 + color: currentcolor; 28 + border-radius: 3px; 29 + background: transparent; 30 + border: none; 31 + display: inline-block; 32 + display: flex; 33 + width: 30px; 34 + height: 30px; 35 + justify-content: center; 36 + align-items: center; 37 + } 38 + 39 + .toast-close:hover { 40 + background: var(--color-hover); 41 + } 42 + 43 + .toast-close:active { 44 + background: var(--color-active); 45 + } 46 + 47 + .toastify-right { 48 + right: 15px; 49 + } 50 + 51 + .toastify-left { 52 + left: 15px; 53 + } 54 + 55 + .toastify-top { 56 + top: -150px; 57 + } 58 + 59 + .toastify-bottom { 60 + bottom: -150px; 61 + } 62 + 63 + .toastify-center { 64 + margin-left: auto; 65 + margin-right: auto; 66 + left: 0; 67 + right: 0; 68 + } 69 + 70 + @media (max-width: 360px) { 71 + .toastify-right, .toastify-left { 72 + margin-left: auto; 73 + margin-right: auto; 74 + left: 0; 75 + right: 0; 76 + max-width: fit-content; 77 + } 78 + }
+16
web_src/css/standalone/devtest.css
··· 1 + .button-sample-groups { 2 + margin: 0; padding: 0; 3 + } 4 + 5 + .button-sample-groups .sample-group { 6 + list-style: none; margin: 0; padding: 0; 7 + } 8 + 9 + .button-sample-groups .sample-group .ui.button { 10 + margin-bottom: 5px; 11 + } 12 + 13 + h1, h2 { 14 + margin: 0; 15 + padding: 10px 0; 16 + }
+2 -1
web_src/js/features/common-global.js
··· 9 9 import {htmlEscape} from 'escape-goat'; 10 10 import {createTippy} from '../modules/tippy.js'; 11 11 import {confirmModal} from './comp/ConfirmModal.js'; 12 + import {showErrorToast} from '../modules/toast.js'; 12 13 13 14 const {appUrl, appSubUrl, csrfToken, i18n} = window.config; 14 15 ··· 439 440 return; 440 441 } 441 442 // should never happen, otherwise there is a bug in code 442 - alert('Nothing to hide'); 443 + showErrorToast('Nothing to hide'); 443 444 }); 444 445 445 446 initGlobalShowModal();
+2 -1
web_src/js/features/comp/ComboMarkdownEditor.js
··· 8 8 import {renderPreviewPanelContent} from '../repo-editor.js'; 9 9 import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js'; 10 10 import {initTextExpander} from './TextExpander.js'; 11 + import {showErrorToast} from '../../modules/toast.js'; 11 12 12 13 let elementIdCounter = 0; 13 14 ··· 26 27 $form[0]?.reportValidity(); 27 28 } else { 28 29 // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places. 29 - alert('Require non-empty content'); 30 + showErrorToast('Require non-empty content'); 30 31 } 31 32 return false; 32 33 }
+3 -2
web_src/js/features/repo-issue-content.js
··· 1 1 import $ from 'jquery'; 2 2 import {svg} from '../svg.js'; 3 + import {showErrorToast} from '../modules/toast.js'; 3 4 4 5 const {appSubUrl, csrfToken} = window.config; 5 6 let i18nTextEdited; ··· 39 40 if (resp.ok) { 40 41 $dialog.modal('hide'); 41 42 } else { 42 - alert(resp.message); 43 + showErrorToast(resp.message); 43 44 } 44 45 }); 45 46 } 46 47 } else { // required by eslint 47 - window.alert(`unknown option item: ${optionItem}`); 48 + showErrorToast(`unknown option item: ${optionItem}`); 48 49 } 49 50 }, 50 51 onHide() {
+2 -1
web_src/js/features/repo-issue-list.js
··· 4 4 import {htmlEscape} from 'escape-goat'; 5 5 import {Sortable} from 'sortablejs'; 6 6 import {confirmModal} from './comp/ConfirmModal.js'; 7 + import {showErrorToast} from '../modules/toast.js'; 7 8 8 9 function initRepoIssueListCheckboxes() { 9 10 const $issueSelectAll = $('.issue-checkbox-all'); ··· 75 76 ).then(() => { 76 77 window.location.reload(); 77 78 }).catch((reason) => { 78 - window.alert(reason.responseJSON.error); 79 + showErrorToast(reason.responseJSON.error); 79 80 }); 80 81 }); 81 82 }
+60
web_src/js/modules/toast.js
··· 1 + import {htmlEscape} from 'escape-goat'; 2 + import {svg} from '../svg.js'; 3 + 4 + const levels = { 5 + info: { 6 + icon: 'octicon-check', 7 + background: 'var(--color-green)', 8 + duration: 2500, 9 + }, 10 + warning: { 11 + icon: 'gitea-exclamation', 12 + background: 'var(--color-orange)', 13 + duration: -1, // requires dismissal to hide 14 + }, 15 + error: { 16 + icon: 'gitea-exclamation', 17 + background: 'var(--color-red)', 18 + duration: -1, // requires dismissal to hide 19 + }, 20 + }; 21 + 22 + // See https://github.com/apvarun/toastify-js#api for options 23 + async function showToast(message, level, {gravity, position, duration, ...other} = {}) { 24 + if (!message) return; 25 + 26 + const {default: Toastify} = await import(/* webpackChunkName: 'toastify' */'toastify-js'); 27 + const {icon, background, duration: levelDuration} = levels[level ?? 'info']; 28 + 29 + const toast = Toastify({ 30 + text: ` 31 + <div class='toast-icon'>${svg(icon)}</div> 32 + <div class='toast-body'>${htmlEscape(message)}</div> 33 + <button class='toast-close'>${svg('octicon-x')}</button> 34 + `, 35 + escapeMarkup: false, 36 + gravity: gravity ?? 'top', 37 + position: position ?? 'center', 38 + duration: duration ?? levelDuration, 39 + style: {background}, 40 + ...other, 41 + }); 42 + 43 + toast.showToast(); 44 + 45 + toast.toastElement.querySelector('.toast-close').addEventListener('click', () => { 46 + toast.removeElement(toast.toastElement); 47 + }); 48 + } 49 + 50 + export async function showInfoToast(message, opts) { 51 + return await showToast(message, 'info', opts); 52 + } 53 + 54 + export async function showWarningToast(message, opts) { 55 + return await showToast(message, 'warning', opts); 56 + } 57 + 58 + export async function showErrorToast(message, opts) { 59 + return await showToast(message, 'error', opts); 60 + }
+17
web_src/js/modules/toast.test.js
··· 1 + import {test, expect} from 'vitest'; 2 + import {showInfoToast, showErrorToast, showWarningToast} from './toast.js'; 3 + 4 + test('showInfoToast', async () => { 5 + await showInfoToast('success ๐Ÿ˜€', {duration: -1}); 6 + expect(document.querySelector('.toastify')).toBeTruthy(); 7 + }); 8 + 9 + test('showWarningToast', async () => { 10 + await showWarningToast('warning ๐Ÿ˜', {duration: -1}); 11 + expect(document.querySelector('.toastify')).toBeTruthy(); 12 + }); 13 + 14 + test('showErrorToast', async () => { 15 + await showErrorToast('error ๐Ÿ™', {duration: -1}); 16 + expect(document.querySelector('.toastify')).toBeTruthy(); 17 + });
+11
web_src/js/standalone/devtest.js
··· 1 + import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js'; 2 + 3 + document.getElementById('info-toast').addEventListener('click', () => { 4 + showInfoToast('success ๐Ÿ˜€'); 5 + }); 6 + document.getElementById('warning-toast').addEventListener('click', () => { 7 + showWarningToast('warning ๐Ÿ˜'); 8 + }); 9 + document.getElementById('error-toast').addEventListener('click', () => { 10 + showErrorToast('error ๐Ÿ™'); 11 + });
+6
webpack.config.js
··· 73 73 'eventsource.sharedworker': [ 74 74 fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.js', import.meta.url)), 75 75 ], 76 + ...(!isProduction && { 77 + devtest: [ 78 + fileURLToPath(new URL('web_src/js/standalone/devtest.js', import.meta.url)), 79 + fileURLToPath(new URL('web_src/css/standalone/devtest.css', import.meta.url)), 80 + ], 81 + }), 76 82 ...themes, 77 83 }, 78 84 devtool: false,