2022-01-28 13:00:11 -08:00
import $ from 'jquery' ;
2023-09-06 02:02:44 +02:00
import '../vendor/jquery.are-you-sure.js' ;
2023-07-30 00:56:45 +02:00
import { clippie } from 'clippie' ;
2022-12-23 17:03:11 +01:00
import { createDropzone } from './dropzone.js' ;
2021-10-17 01:28:04 +08:00
import { initCompColorPicker } from './comp/ColorPicker.js' ;
2022-03-30 13:52:24 +08:00
import { showGlobalErrorMessage } from '../bootstrap.js' ;
2022-08-05 18:08:29 +08:00
import { handleGlobalEnterQuickSubmit } from './comp/QuickSubmit.js' ;
2023-01-22 05:14:43 +01:00
import { svg } from '../svg.js' ;
2023-02-19 12:06:14 +08:00
import { hideElem , showElem , toggleElem } from '../utils/dom.js' ;
2023-05-22 00:06:17 +02:00
import { htmlEscape } from 'escape-goat' ;
2023-08-22 11:30:02 +09:00
import { showTemporaryTooltip } from '../modules/tippy.js' ;
2023-06-19 15:46:50 +08:00
import { confirmModal } from './comp/ConfirmModal.js' ;
2023-06-27 04:45:24 +02:00
import { showErrorToast } from '../modules/toast.js' ;
2023-09-19 02:50:30 +02:00
import { request } from '../modules/fetch.js' ;
2021-10-17 01:28:04 +08:00
2023-06-16 14:32:43 +08:00
const { appUrl , appSubUrl , csrfToken , i18n } = window . config ;
2021-10-17 01:28:04 +08:00
export function initGlobalFormDirtyLeaveConfirm ( ) {
// Warn users that try to leave a page after entering data into a form.
// Except on sign-in pages, and for forms marked as 'ignore-dirty'.
if ( $ ( '.user.signin' ) . length === 0 ) {
$ ( 'form:not(.ignore-dirty)' ) . areYouSure ( ) ;
}
}
export function initHeadNavbarContentToggle ( ) {
2023-06-09 11:10:51 +02:00
const navbar = document . getElementById ( 'navbar' ) ;
const btn = document . getElementById ( 'navbar-expand-toggle' ) ;
if ( ! navbar || ! btn ) return ;
btn . addEventListener ( 'click' , ( ) => {
const isExpanded = btn . classList . contains ( 'active' ) ;
navbar . classList . toggle ( 'navbar-menu-open' , ! isExpanded ) ;
btn . classList . toggle ( 'active' , ! isExpanded ) ;
2021-10-17 01:28:04 +08:00
} ) ;
}
export function initFootLanguageMenu ( ) {
function linkLanguageAction ( ) {
const $this = $ ( this ) ;
2022-11-09 15:40:26 +08:00
$ . get ( $this . data ( 'url' ) ) . always ( ( ) => {
2021-10-17 01:28:04 +08:00
window . location . reload ( ) ;
} ) ;
}
$ ( '.language-menu a[lang]' ) . on ( 'click' , linkLanguageAction ) ;
}
export function initGlobalEnterQuickSubmit ( ) {
2022-05-20 10:26:04 +08:00
$ ( document ) . on ( 'keydown' , '.js-quick-submit' , ( e ) => {
if ( ( ( e . ctrlKey && ! e . altKey ) || e . metaKey ) && ( e . key === 'Enter' ) ) {
handleGlobalEnterQuickSubmit ( e . target ) ;
return false ;
2021-10-17 01:28:04 +08:00
}
} ) ;
}
export function initGlobalButtonClickOnEnter ( ) {
2023-05-21 22:47:41 +02:00
$ ( document ) . on ( 'keypress' , 'div.ui.button,span.ui.button' , ( e ) => {
if ( e . code === ' ' || e . code === 'Enter' ) {
2021-10-17 01:28:04 +08:00
$ ( e . target ) . trigger ( 'click' ) ;
2023-02-10 01:11:16 +08:00
e . preventDefault ( ) ;
2021-10-17 01:28:04 +08:00
}
} ) ;
}
2023-08-22 11:30:02 +09:00
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
2023-06-16 14:32:43 +08:00
// more details are in the backend's fetch-redirect handler
2023-08-22 11:30:02 +09:00
function fetchActionDoRedirect ( redirect ) {
2023-06-16 14:32:43 +08:00
const form = document . createElement ( 'form' ) ;
const input = document . createElement ( 'input' ) ;
form . method = 'post' ;
form . action = ` ${ appSubUrl } /-/fetch-redirect ` ;
input . type = 'hidden' ;
input . name = 'redirect' ;
input . value = redirect ;
form . append ( input ) ;
document . body . append ( form ) ;
form . submit ( ) ;
}
2023-08-22 11:30:02 +09:00
async function fetchActionDoRequest ( actionElem , url , opt ) {
try {
2023-09-19 02:50:30 +02:00
const resp = await request ( url , opt ) ;
2023-08-22 11:30:02 +09:00
if ( resp . status === 200 ) {
let { redirect } = await resp . json ( ) ;
redirect = redirect || actionElem . getAttribute ( 'data-redirect' ) ;
actionElem . classList . remove ( 'dirty' ) ; // remove the areYouSure check before reloading
if ( redirect ) {
fetchActionDoRedirect ( redirect ) ;
} else {
window . location . reload ( ) ;
}
} else if ( resp . status >= 400 && resp . status < 500 ) {
const data = await resp . json ( ) ;
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
2023-08-23 15:25:13 +08:00
showErrorToast ( data . errorMessage || ` server error: ${ resp . status } ` ) ;
2023-08-22 11:30:02 +09:00
} else {
2023-08-23 15:25:13 +08:00
showErrorToast ( ` server error: ${ resp . status } ` ) ;
2023-08-22 11:30:02 +09:00
}
} catch ( e ) {
console . error ( 'error when doRequest' , e ) ;
actionElem . classList . remove ( 'is-loading' , 'small-loading-icon' ) ;
2023-08-23 15:25:13 +08:00
showErrorToast ( i18n . network _error ) ;
2023-08-22 11:30:02 +09:00
}
}
2023-06-14 16:01:37 +08:00
async function formFetchAction ( e ) {
if ( ! e . target . classList . contains ( 'form-fetch-action' ) ) return ;
e . preventDefault ( ) ;
const formEl = e . target ;
if ( formEl . classList . contains ( 'is-loading' ) ) return ;
formEl . classList . add ( 'is-loading' ) ;
if ( formEl . clientHeight < 50 ) {
formEl . classList . add ( 'small-loading-icon' ) ;
}
const formMethod = formEl . getAttribute ( 'method' ) || 'get' ;
const formActionUrl = formEl . getAttribute ( 'action' ) ;
const formData = new FormData ( formEl ) ;
const [ submitterName , submitterValue ] = [ e . submitter ? . getAttribute ( 'name' ) , e . submitter ? . getAttribute ( 'value' ) ] ;
if ( submitterName ) {
formData . append ( submitterName , submitterValue || '' ) ;
}
let reqUrl = formActionUrl ;
2023-09-19 02:50:30 +02:00
const reqOpt = { method : formMethod . toUpperCase ( ) } ;
2023-06-14 16:01:37 +08:00
if ( formMethod . toLowerCase ( ) === 'get' ) {
const params = new URLSearchParams ( ) ;
for ( const [ key , value ] of formData ) {
params . append ( key , value . toString ( ) ) ;
}
const pos = reqUrl . indexOf ( '?' ) ;
if ( pos !== - 1 ) {
reqUrl = reqUrl . slice ( 0 , pos ) ;
}
reqUrl += ` ? ${ params . toString ( ) } ` ;
} else {
reqOpt . body = formData ;
}
2023-08-22 11:30:02 +09:00
await fetchActionDoRequest ( formEl , reqUrl , reqOpt ) ;
2023-06-14 16:01:37 +08:00
}
2021-10-17 01:28:04 +08:00
export function initGlobalCommon ( ) {
// Semantic UI modules.
2022-06-04 05:38:26 +08:00
const $uiDropdowns = $ ( '.ui.dropdown' ) ;
2023-03-17 11:08:05 +08:00
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
2023-03-26 19:31:26 +08:00
$uiDropdowns . filter ( ':not(.custom)' ) . dropdown ( ) ;
2023-03-17 11:08:05 +08:00
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
2022-06-04 05:38:26 +08:00
$uiDropdowns . filter ( '.jump' ) . dropdown ( {
2021-10-17 01:28:04 +08:00
action : 'hide' ,
onShow ( ) {
2022-08-09 14:37:34 +02:00
// hide associated tooltip while dropdown is open
this . _tippy ? . hide ( ) ;
this . _tippy ? . disable ( ) ;
} ,
onHide ( ) {
this . _tippy ? . enable ( ) ;
2023-03-17 11:08:05 +08:00
// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
setTimeout ( ( ) => {
const $dropdown = $ ( this ) ;
if ( $dropdown . dropdown ( 'is hidden' ) ) {
$ ( this ) . find ( '.menu > .item' ) . each ( ( _ , item ) => {
item . _tippy ? . hide ( ) ;
} ) ;
}
} , 2000 ) ;
2021-10-17 01:28:04 +08:00
} ,
} ) ;
2023-03-17 11:08:05 +08:00
2023-03-31 01:53:51 +08:00
// Special popup-directions, prevent Fomantic from guessing the popup direction.
// With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
// if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
// eg: Issue List "Sort" dropdown
// But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
// which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
// eg: the "Create New Repo" menu on the navbar.
2023-03-26 19:31:26 +08:00
$uiDropdowns . filter ( '.upward' ) . dropdown ( 'setting' , 'direction' , 'upward' ) ;
2023-03-31 01:53:51 +08:00
$uiDropdowns . filter ( '.downward' ) . dropdown ( 'setting' , 'direction' , 'downward' ) ;
2023-03-17 11:08:05 +08:00
2023-03-22 10:52:01 +08:00
$ ( '.ui.checkbox' ) . checkbox ( ) ;
2021-11-18 04:26:50 +01:00
2021-10-17 01:28:04 +08:00
$ ( '.tabular.menu .item' ) . tab ( ) ;
2023-06-14 16:01:37 +08:00
document . addEventListener ( 'submit' , formFetchAction ) ;
2023-08-22 11:30:02 +09:00
document . addEventListener ( 'click' , linkAction ) ;
2021-10-17 01:28:04 +08:00
}
2021-11-09 17:27:25 +08:00
export function initGlobalDropzone ( ) {
2021-10-17 01:28:04 +08:00
// Dropzone
for ( const el of document . querySelectorAll ( '.dropzone' ) ) {
const $dropzone = $ ( el ) ;
2021-11-09 17:27:25 +08:00
const _promise = createDropzone ( el , {
2021-10-17 01:28:04 +08:00
url : $dropzone . data ( 'upload-url' ) ,
2021-10-21 15:37:43 +08:00
headers : { 'X-Csrf-Token' : csrfToken } ,
2021-10-17 01:28:04 +08:00
maxFiles : $dropzone . data ( 'max-file' ) ,
maxFilesize : $dropzone . data ( 'max-size' ) ,
acceptedFiles : ( [ '*/*' , '' ] . includes ( $dropzone . data ( 'accepts' ) ) ) ? null : $dropzone . data ( 'accepts' ) ,
addRemoveLinks : true ,
dictDefaultMessage : $dropzone . data ( 'default-message' ) ,
dictInvalidFileType : $dropzone . data ( 'invalid-input-type' ) ,
dictFileTooBig : $dropzone . data ( 'file-too-big' ) ,
dictRemoveFile : $dropzone . data ( 'remove-file' ) ,
timeout : 0 ,
thumbnailMethod : 'contain' ,
thumbnailWidth : 480 ,
thumbnailHeight : 480 ,
init ( ) {
2022-06-25 21:49:56 +02:00
this . on ( 'success' , ( file , data ) => {
file . uuid = data . uuid ;
2021-10-17 01:28:04 +08:00
const input = $ ( ` <input id=" ${ data . uuid } " name="files" type="hidden"> ` ) . val ( data . uuid ) ;
$dropzone . find ( '.files' ) . append ( input ) ;
2023-01-19 06:33:40 +01:00
// Create a "Copy Link" element, to conveniently copy the image
// or file link as Markdown to the clipboard
2023-01-22 05:14:43 +01:00
const copyLinkElement = document . createElement ( 'div' ) ;
2023-07-08 17:53:56 +08:00
copyLinkElement . className = 'gt-text-center' ;
2023-01-22 05:14:43 +01:00
// The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone
copyLinkElement . innerHTML = ` <a href="#" style="cursor: pointer;"> ${ svg ( 'octicon-copy' , 14 , 'copy link' ) } Copy link</a> ` ;
2023-07-30 00:56:45 +02:00
copyLinkElement . addEventListener ( 'click' , async ( e ) => {
2023-01-19 06:33:40 +01:00
e . preventDefault ( ) ;
let fileMarkdown = ` [ ${ file . name } ](/attachments/ ${ file . uuid } ) ` ;
if ( file . type . startsWith ( 'image/' ) ) {
fileMarkdown = ` ! ${ fileMarkdown } ` ;
2023-05-22 00:06:17 +02:00
} else if ( file . type . startsWith ( 'video/' ) ) {
fileMarkdown = ` <video src="/attachments/ ${ file . uuid } " title=" ${ htmlEscape ( file . name ) } " controls></video> ` ;
2023-01-19 06:33:40 +01:00
}
2023-07-30 00:56:45 +02:00
const success = await clippie ( fileMarkdown ) ;
showTemporaryTooltip ( e . target , success ? i18n . copy _success : i18n . copy _error ) ;
2023-01-19 06:33:40 +01:00
} ) ;
2023-05-09 04:35:49 +02:00
file . previewTemplate . append ( copyLinkElement ) ;
2021-10-17 01:28:04 +08:00
} ) ;
this . on ( 'removedfile' , ( file ) => {
$ ( ` # ${ file . uuid } ` ) . remove ( ) ;
if ( $dropzone . data ( 'remove-url' ) ) {
$ . post ( $dropzone . data ( 'remove-url' ) , {
file : file . uuid ,
2021-10-21 15:37:43 +08:00
_csrf : csrfToken ,
2021-10-17 01:28:04 +08:00
} ) ;
}
} ) ;
} ,
} ) ;
}
}
2023-06-19 15:46:50 +08:00
async function linkAction ( e ) {
2023-06-13 20:10:10 +08:00
// A "link-action" can post AJAX request to its "data-url"
2023-06-18 23:23:18 +08:00
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
2023-08-22 11:30:02 +09:00
const el = e . target . closest ( '.link-action' ) ;
if ( ! el ) return ;
2023-06-13 20:10:10 +08:00
2023-08-22 11:30:02 +09:00
e . preventDefault ( ) ;
const url = el . getAttribute ( 'data-url' ) ;
const doRequest = async ( ) => {
el . disabled = true ;
2023-09-19 02:50:30 +02:00
await fetchActionDoRequest ( el , url , { method : 'POST' } ) ;
2023-08-22 11:30:02 +09:00
el . disabled = false ;
2023-06-13 20:10:10 +08:00
} ;
2023-08-22 11:30:02 +09:00
const modalConfirmContent = htmlEscape ( el . getAttribute ( 'data-modal-confirm' ) || '' ) ;
2023-06-19 15:46:50 +08:00
if ( ! modalConfirmContent ) {
2023-08-22 11:30:02 +09:00
await doRequest ( ) ;
2023-06-13 20:10:10 +08:00
return ;
}
2023-08-22 11:30:02 +09:00
const isRisky = el . classList . contains ( 'red' ) || el . classList . contains ( 'yellow' ) || el . classList . contains ( 'orange' ) || el . classList . contains ( 'negative' ) ;
2023-09-19 00:05:31 +02:00
if ( await confirmModal ( { content : modalConfirmContent , buttonColor : isRisky ? 'orange' : 'primary' } ) ) {
2023-08-22 11:30:02 +09:00
await doRequest ( ) ;
2023-06-19 15:46:50 +08:00
}
2023-06-13 20:10:10 +08:00
}
2021-10-17 01:28:04 +08:00
export function initGlobalLinkActions ( ) {
2023-03-14 04:34:09 +01:00
function showDeletePopup ( e ) {
e . preventDefault ( ) ;
2021-10-17 01:28:04 +08:00
const $this = $ ( this ) ;
const dataArray = $this . data ( ) ;
let filter = '' ;
2023-04-19 00:49:49 +08:00
if ( $this . attr ( 'data-modal-id' ) ) {
filter += ` # ${ $this . attr ( 'data-modal-id' ) } ` ;
2021-10-17 01:28:04 +08:00
}
const dialog = $ ( ` .delete.modal ${ filter } ` ) ;
dialog . find ( '.name' ) . text ( $this . data ( 'name' ) ) ;
for ( const [ key , value ] of Object . entries ( dataArray ) ) {
if ( key && key . startsWith ( 'data' ) ) {
dialog . find ( ` . ${ key } ` ) . text ( value ) ;
}
}
dialog . modal ( {
closable : false ,
onApprove ( ) {
if ( $this . data ( 'type' ) === 'form' ) {
2022-01-16 19:19:26 +08:00
$ ( $this . data ( 'form' ) ) . trigger ( 'submit' ) ;
2021-10-17 01:28:04 +08:00
return ;
}
const postData = {
2021-10-21 15:37:43 +08:00
_csrf : csrfToken ,
2021-10-17 01:28:04 +08:00
} ;
for ( const [ key , value ] of Object . entries ( dataArray ) ) {
if ( key && key . startsWith ( 'data' ) ) {
2022-02-18 07:50:36 +01:00
postData [ key . slice ( 4 ) ] = value ;
2021-10-17 01:28:04 +08:00
}
if ( key === 'id' ) {
postData [ 'id' ] = value ;
}
}
$ . post ( $this . data ( 'url' ) , postData ) . done ( ( data ) => {
window . location . href = data . redirect ;
} ) ;
}
} ) . modal ( 'show' ) ;
}
// Helpers.
$ ( '.delete-button' ) . on ( 'click' , showDeletePopup ) ;
}
2023-06-21 06:54:15 +08:00
function initGlobalShowModal ( ) {
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
// * First, try to query '#target'
// * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
$ ( '.show-modal' ) . on ( 'click' , function ( e ) {
e . preventDefault ( ) ;
const $el = $ ( this ) ;
const modalSelector = $el . attr ( 'data-modal' ) ;
const $modal = $ ( modalSelector ) ;
if ( ! $modal . length ) {
throw new Error ( 'no modal for this action' ) ;
}
const modalAttrPrefix = 'data-modal-' ;
for ( const attrib of this . attributes ) {
if ( ! attrib . name . startsWith ( modalAttrPrefix ) ) {
continue ;
}
const attrTargetCombo = attrib . name . substring ( modalAttrPrefix . length ) ;
const [ attrTargetName , attrTargetAttr ] = attrTargetCombo . split ( '.' ) ;
// try to find target by: "#target" -> ".target" -> "target tag"
let $attrTarget = $modal . find ( ` # ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) $attrTarget = $modal . find ( ` . ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) $attrTarget = $modal . find ( ` ${ attrTargetName } ` ) ;
if ( ! $attrTarget . length ) continue ; // TODO: show errors in dev mode to remind developers that there is a bug
if ( attrTargetAttr ) {
$attrTarget [ 0 ] [ attrTargetAttr ] = attrib . value ;
} else if ( $attrTarget . is ( 'input' ) || $attrTarget . is ( 'textarea' ) ) {
$attrTarget . val ( attrib . value ) ; // FIXME: add more supports like checkbox
} else {
$attrTarget . text ( attrib . value ) ; // FIXME: it should be more strict here, only handle div/span/p
}
}
const colorPickers = $modal . find ( '.color-picker' ) ;
if ( colorPickers . length > 0 ) {
initCompColorPicker ( ) ; // FIXME: this might cause duplicate init
}
$modal . modal ( 'setting' , {
onApprove : ( ) => {
// "form-fetch-action" can handle network errors gracefully,
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
if ( $modal . find ( '.form-fetch-action' ) . length ) return false ;
} ,
} ) . modal ( 'show' ) ;
} ) ;
}
2021-10-17 01:28:04 +08:00
export function initGlobalButtons ( ) {
2023-03-14 04:34:09 +01:00
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
2023-03-24 16:37:56 +08:00
$ ( document ) . on ( 'click' , 'form button.ui.cancel.button' , ( e ) => {
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
} ) ;
2023-09-10 18:27:23 +08:00
$ ( '.show-panel' ) . on ( 'click' , function ( e ) {
// a '.show-panel' element can show a panel, by `data-panel="selector"`
// if it has "toggle" class, it toggles the panel
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
2023-03-26 20:06:11 +08:00
const sel = $ ( this ) . attr ( 'data-panel' ) ;
if ( this . classList . contains ( 'toggle' ) ) {
toggleElem ( sel ) ;
} else {
showElem ( sel ) ;
}
2021-10-17 01:28:04 +08:00
} ) ;
2023-09-10 18:27:23 +08:00
$ ( '.hide-panel' ) . on ( 'click' , function ( e ) {
// a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"`
2023-03-14 04:34:09 +01:00
e . preventDefault ( ) ;
2022-01-07 01:18:52 +00:00
let sel = $ ( this ) . attr ( 'data-panel' ) ;
if ( sel ) {
2023-02-19 12:06:14 +08:00
hideElem ( $ ( sel ) ) ;
2022-01-07 01:18:52 +00:00
return ;
}
sel = $ ( this ) . attr ( 'data-panel-closest' ) ;
if ( sel ) {
2023-02-19 12:06:14 +08:00
hideElem ( $ ( this ) . closest ( sel ) ) ;
2022-01-07 01:18:52 +00:00
return ;
}
// should never happen, otherwise there is a bug in code
2023-06-27 04:45:24 +02:00
showErrorToast ( 'Nothing to hide' ) ;
2021-10-17 01:28:04 +08:00
} ) ;
2023-06-21 06:54:15 +08:00
initGlobalShowModal ( ) ;
2021-10-17 01:28:04 +08:00
}
2022-03-30 13:52:24 +08:00
/**
* Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
* * Cross-origin API request without correct cookie
* * Incorrect href in <a>
* * ...
* So we check whether current URL starts with AppUrl(ROOT_URL).
* If they don't match, show a warning to users.
*/
export function checkAppUrl ( ) {
const curUrl = window . location . href ;
2022-07-27 17:19:10 +08:00
// some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
if ( curUrl . startsWith ( appUrl ) || ` ${ curUrl } / ` === appUrl ) {
2022-03-30 13:52:24 +08:00
return ;
}
2023-02-10 00:14:45 +08:00
showGlobalErrorMessage ( ` Your ROOT_URL in app.ini is " ${ appUrl } ", it's unlikely matching the site you are visiting.
2023-07-19 06:14:30 +08:00
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in. ` ) ;
2022-03-30 13:52:24 +08:00
}