gitea源码

common-fetch-action.ts 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import {request} from '../modules/fetch.ts';
  2. import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
  3. import {addDelegatedEventListener, createElementFromHTML, submitEventSubmitter} from '../utils/dom.ts';
  4. import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
  5. import type {RequestOpts} from '../types.ts';
  6. import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
  7. const {appSubUrl} = window.config;
  8. // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
  9. // more details are in the backend's fetch-redirect handler
  10. function fetchActionDoRedirect(redirect: string) {
  11. const form = document.createElement('form');
  12. const input = document.createElement('input');
  13. form.method = 'post';
  14. form.action = `${appSubUrl}/-/fetch-redirect`;
  15. input.type = 'hidden';
  16. input.name = 'redirect';
  17. input.value = redirect;
  18. form.append(input);
  19. document.body.append(form);
  20. form.submit();
  21. }
  22. async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
  23. const showErrorForResponse = (code: number, message: string) => {
  24. showErrorToast(`Error ${code || 'request'}: ${message}`);
  25. };
  26. let respStatus = 0;
  27. let respText = '';
  28. try {
  29. hideToastsAll();
  30. const resp = await request(url, opt);
  31. respStatus = resp.status;
  32. respText = await resp.text();
  33. const respJson = JSON.parse(respText);
  34. if (respStatus === 200) {
  35. let {redirect} = respJson;
  36. redirect = redirect || actionElem.getAttribute('data-redirect');
  37. ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
  38. if (redirect) {
  39. fetchActionDoRedirect(redirect);
  40. } else {
  41. window.location.reload();
  42. }
  43. return;
  44. }
  45. if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
  46. // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
  47. // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
  48. showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
  49. } else {
  50. showErrorForResponse(respStatus, respText);
  51. }
  52. } catch (e) {
  53. if (e.name === 'SyntaxError') {
  54. showErrorForResponse(respStatus, (respText || '').substring(0, 100));
  55. } else if (e.name !== 'AbortError') {
  56. console.error('fetchActionDoRequest error', e);
  57. showErrorForResponse(respStatus, `${e}`);
  58. }
  59. }
  60. actionElem.classList.remove('is-loading', 'loading-icon-2px');
  61. }
  62. async function onFormFetchActionSubmit(formEl: HTMLFormElement, e: SubmitEvent) {
  63. e.preventDefault();
  64. await submitFormFetchAction(formEl, submitEventSubmitter(e));
  65. }
  66. export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitter?: HTMLElement) {
  67. if (formEl.classList.contains('is-loading')) return;
  68. formEl.classList.add('is-loading');
  69. if (formEl.clientHeight < 50) {
  70. formEl.classList.add('loading-icon-2px');
  71. }
  72. const formMethod = formEl.getAttribute('method') || 'get';
  73. const formActionUrl = formEl.getAttribute('action') || window.location.href;
  74. const formData = new FormData(formEl);
  75. const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
  76. if (submitterName) {
  77. formData.append(submitterName, submitterValue || '');
  78. }
  79. let reqUrl = formActionUrl;
  80. const reqOpt = {
  81. method: formMethod.toUpperCase(),
  82. body: null as FormData | null,
  83. };
  84. if (formMethod.toLowerCase() === 'get') {
  85. const params = new URLSearchParams();
  86. for (const [key, value] of formData) {
  87. params.append(key, value.toString());
  88. }
  89. const pos = reqUrl.indexOf('?');
  90. if (pos !== -1) {
  91. reqUrl = reqUrl.slice(0, pos);
  92. }
  93. reqUrl += `?${params.toString()}`;
  94. } else {
  95. reqOpt.body = formData;
  96. }
  97. await fetchActionDoRequest(formEl, reqUrl, reqOpt);
  98. }
  99. async function onLinkActionClick(el: HTMLElement, e: Event) {
  100. // A "link-action" can post AJAX request to its "data-url"
  101. // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
  102. // If the "link-action" has "data-modal-confirm" attribute, a "confirm modal dialog" will be shown before taking action.
  103. // Attribute "data-modal-confirm" can be a modal element by "#the-modal-id", or a string content for the modal dialog.
  104. e.preventDefault();
  105. const url = el.getAttribute('data-url');
  106. const doRequest = async () => {
  107. if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but "A" doesn't have the "disabled" attribute
  108. await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'});
  109. if ('disabled' in el) el.disabled = false;
  110. };
  111. let elModal: HTMLElement | null = null;
  112. const dataModalConfirm = el.getAttribute('data-modal-confirm') || '';
  113. if (dataModalConfirm.startsWith('#')) {
  114. // eslint-disable-next-line unicorn/prefer-query-selector
  115. elModal = document.getElementById(dataModalConfirm.substring(1));
  116. if (elModal) {
  117. elModal = createElementFromHTML(elModal.outerHTML);
  118. elModal.removeAttribute('id');
  119. }
  120. }
  121. if (!elModal) {
  122. const modalConfirmContent = dataModalConfirm || el.getAttribute('data-modal-confirm-content') || '';
  123. if (modalConfirmContent) {
  124. const isRisky = el.classList.contains('red') || el.classList.contains('negative');
  125. elModal = createConfirmModal({
  126. header: el.getAttribute('data-modal-confirm-header') || '',
  127. content: modalConfirmContent,
  128. confirmButtonColor: isRisky ? 'red' : 'primary',
  129. });
  130. }
  131. }
  132. if (!elModal) {
  133. await doRequest();
  134. return;
  135. }
  136. if (await confirmModal(elModal)) {
  137. await doRequest();
  138. }
  139. }
  140. export function initGlobalFetchAction() {
  141. addDelegatedEventListener(document, 'submit', '.form-fetch-action', onFormFetchActionSubmit);
  142. addDelegatedEventListener(document, 'click', '.link-action', onLinkActionClick);
  143. }