diff options
Diffstat (limited to 'web_src/js/features/common-fetch-action.ts')
-rw-r--r-- | web_src/js/features/common-fetch-action.ts | 96 |
1 files changed, 62 insertions, 34 deletions
diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 2da481e521..3ca361b6e2 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -1,11 +1,11 @@ import {request} from '../modules/fetch.ts'; -import {showErrorToast} from '../modules/toast.ts'; -import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts'; -import {confirmModal} from './comp/ConfirmModal.ts'; +import {hideToastsAll, showErrorToast} from '../modules/toast.ts'; +import {addDelegatedEventListener, createElementFromHTML, submitEventSubmitter} from '../utils/dom.ts'; +import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts'; import type {RequestOpts} from '../types.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -const {appSubUrl, i18n} = window.config; +const {appSubUrl} = window.config; // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" // more details are in the backend's fetch-redirect handler @@ -23,10 +23,20 @@ function fetchActionDoRedirect(redirect: string) { } async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { + const showErrorForResponse = (code: number, message: string) => { + showErrorToast(`Error ${code || 'request'}: ${message}`); + }; + + let respStatus = 0; + let respText = ''; try { + hideToastsAll(); const resp = await request(url, opt); - if (resp.status === 200) { - let {redirect} = await resp.json(); + respStatus = resp.status; + respText = await resp.text(); + const respJson = JSON.parse(respText); + if (respStatus === 200) { + let {redirect} = respJson; redirect = redirect || actionElem.getAttribute('data-redirect'); ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading if (redirect) { @@ -35,29 +45,32 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R window.location.reload(); } return; - } else if (resp.status >= 400 && resp.status < 500) { - const data = await resp.json(); + } + + if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) { // 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. - if (data.errorMessage) { - showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); - } else { - showErrorToast(`server error: ${resp.status}`); - } + showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'}); } else { - showErrorToast(`server error: ${resp.status}`); + showErrorForResponse(respStatus, respText); } } catch (e) { - if (e.name !== 'AbortError') { - console.error('error when doRequest', e); - showErrorToast(`${i18n.network_error} ${e}`); + if (e.name === 'SyntaxError') { + showErrorForResponse(respStatus, (respText || '').substring(0, 100)); + } else if (e.name !== 'AbortError') { + console.error('fetchActionDoRequest error', e); + showErrorForResponse(respStatus, `${e}`); } } actionElem.classList.remove('is-loading', 'loading-icon-2px'); } -async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { +async function onFormFetchActionSubmit(formEl: HTMLFormElement, e: SubmitEvent) { e.preventDefault(); + await submitFormFetchAction(formEl, submitEventSubmitter(e)); +} + +export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitter?: HTMLElement) { if (formEl.classList.contains('is-loading')) return; formEl.classList.add('is-loading'); @@ -66,9 +79,8 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { } const formMethod = formEl.getAttribute('method') || 'get'; - const formActionUrl = formEl.getAttribute('action'); + const formActionUrl = formEl.getAttribute('action') || window.location.href; const formData = new FormData(formEl); - const formSubmitter = submitEventSubmitter(e); const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; if (submitterName) { formData.append(submitterName, submitterValue || ''); @@ -96,36 +108,52 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { await fetchActionDoRequest(formEl, reqUrl, reqOpt); } -async function linkAction(el: HTMLElement, e: Event) { +async function onLinkActionClick(el: HTMLElement, e: Event) { // A "link-action" can post AJAX request to its "data-url" // 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. + // If the "link-action" has "data-modal-confirm" attribute, a "confirm modal dialog" will be shown before taking action. + // Attribute "data-modal-confirm" can be a modal element by "#the-modal-id", or a string content for the modal dialog. e.preventDefault(); const url = el.getAttribute('data-url'); const doRequest = async () => { - if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute + if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but "A" doesn't have the "disabled" attribute await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'}); if ('disabled' in el) el.disabled = false; }; - const modalConfirmContent = el.getAttribute('data-modal-confirm') || - el.getAttribute('data-modal-confirm-content') || ''; - if (!modalConfirmContent) { + let elModal: HTMLElement | null = null; + const dataModalConfirm = el.getAttribute('data-modal-confirm') || ''; + if (dataModalConfirm.startsWith('#')) { + // eslint-disable-next-line unicorn/prefer-query-selector + elModal = document.getElementById(dataModalConfirm.substring(1)); + if (elModal) { + elModal = createElementFromHTML(elModal.outerHTML); + elModal.removeAttribute('id'); + } + } + if (!elModal) { + const modalConfirmContent = dataModalConfirm || el.getAttribute('data-modal-confirm-content') || ''; + if (modalConfirmContent) { + const isRisky = el.classList.contains('red') || el.classList.contains('negative'); + elModal = createConfirmModal({ + header: el.getAttribute('data-modal-confirm-header') || '', + content: modalConfirmContent, + confirmButtonColor: isRisky ? 'red' : 'primary', + }); + } + } + + if (!elModal) { await doRequest(); return; } - const isRisky = el.classList.contains('red') || el.classList.contains('negative'); - if (await confirmModal({ - header: el.getAttribute('data-modal-confirm-header') || '', - content: modalConfirmContent, - confirmButtonColor: isRisky ? 'red' : 'primary', - })) { + if (await confirmModal(elModal)) { await doRequest(); } } export function initGlobalFetchAction() { - addDelegatedEventListener(document, 'submit', '.form-fetch-action', formFetchAction); - addDelegatedEventListener(document, 'click', '.link-action', linkAction); + addDelegatedEventListener(document, 'submit', '.form-fetch-action', onFormFetchActionSubmit); + addDelegatedEventListener(document, 'click', '.link-action', onLinkActionClick); } |