code.gitea.io/gitea@v1.22.3/web_src/js/features/repo-issue-content.js (about) 1 import $ from 'jquery'; 2 import {svg} from '../svg.js'; 3 import {showErrorToast} from '../modules/toast.js'; 4 import {GET, POST} from '../modules/fetch.js'; 5 import {showElem} from '../utils/dom.js'; 6 7 const {appSubUrl} = window.config; 8 let i18nTextEdited; 9 let i18nTextOptions; 10 let i18nTextDeleteFromHistory; 11 let i18nTextDeleteFromHistoryConfirm; 12 13 function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleHtml) { 14 let $dialog = $('.content-history-detail-dialog'); 15 if ($dialog.length) return; 16 17 $dialog = $(` 18 <div class="ui modal content-history-detail-dialog"> 19 ${svg('octicon-x', 16, 'close icon inside')} 20 <div class="header tw-flex tw-items-center tw-justify-between"> 21 <div>${itemTitleHtml}</div> 22 <div class="ui dropdown dialog-header-options tw-mr-8 tw-hidden"> 23 ${i18nTextOptions} 24 ${svg('octicon-triangle-down', 14, 'dropdown icon')} 25 <div class="menu"> 26 <div class="item red text" data-option-item="delete">${i18nTextDeleteFromHistory}</div> 27 </div> 28 </div> 29 </div> 30 <div class="comment-diff-data is-loading"></div> 31 </div>`); 32 $dialog.appendTo($('body')); 33 $dialog.find('.dialog-header-options').dropdown({ 34 showOnFocus: false, 35 allowReselection: true, 36 async onChange(_value, _text, $item) { 37 const optionItem = $item.data('option-item'); 38 if (optionItem === 'delete') { 39 if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { 40 try { 41 const params = new URLSearchParams(); 42 params.append('comment_id', commentId); 43 params.append('history_id', historyId); 44 45 const response = await POST(`${issueBaseUrl}/content-history/soft-delete?${params.toString()}`); 46 const resp = await response.json(); 47 48 if (resp.ok) { 49 $dialog.modal('hide'); 50 } else { 51 showErrorToast(resp.message); 52 } 53 } catch (error) { 54 console.error('Error:', error); 55 showErrorToast('An error occurred while deleting the history.'); 56 } 57 } 58 } else { // required by eslint 59 showErrorToast(`unknown option item: ${optionItem}`); 60 } 61 }, 62 onHide() { 63 $(this).dropdown('clear', true); 64 }, 65 }); 66 $dialog.modal({ 67 async onShow() { 68 try { 69 const params = new URLSearchParams(); 70 params.append('comment_id', commentId); 71 params.append('history_id', historyId); 72 73 const url = `${issueBaseUrl}/content-history/detail?${params.toString()}`; 74 const response = await GET(url); 75 const resp = await response.json(); 76 77 const commentDiffData = $dialog.find('.comment-diff-data')[0]; 78 commentDiffData?.classList.remove('is-loading'); 79 commentDiffData.innerHTML = resp.diffHtml; 80 // there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden. 81 if (resp.canSoftDelete) { 82 showElem($dialog.find('.dialog-header-options')); 83 } 84 } catch (error) { 85 console.error('Error:', error); 86 } 87 }, 88 onHidden() { 89 $dialog.remove(); 90 }, 91 }).modal('show'); 92 } 93 94 function showContentHistoryMenu(issueBaseUrl, $item, commentId) { 95 const $headerLeft = $item.find('.comment-header-left'); 96 const menuHtml = ` 97 <div class="ui dropdown interact-fg content-history-menu" data-comment-id="${commentId}"> 98 • ${i18nTextEdited}${svg('octicon-triangle-down', 14, 'dropdown icon')} 99 <div class="menu"> 100 </div> 101 </div>`; 102 103 $headerLeft.find(`.content-history-menu`).remove(); 104 $headerLeft.append($(menuHtml)); 105 $headerLeft.find('.dropdown').dropdown({ 106 action: 'hide', 107 apiSettings: { 108 cache: false, 109 url: `${issueBaseUrl}/content-history/list?comment_id=${commentId}`, 110 }, 111 saveRemoteData: false, 112 onHide() { 113 $(this).dropdown('change values', null); 114 }, 115 onChange(value, itemHtml, $item) { 116 if (value && !$item.find('[data-history-is-deleted=1]').length) { 117 showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml); 118 } 119 }, 120 }); 121 } 122 123 export async function initRepoIssueContentHistory() { 124 const issueIndex = $('#issueIndex').val(); 125 if (!issueIndex) return; 126 127 const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content 128 const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments 129 if (!$itemIssue.length && !$comments.length) return; 130 131 const repoLink = $('#repolink').val(); 132 const issueBaseUrl = `${appSubUrl}/${repoLink}/issues/${issueIndex}`; 133 134 try { 135 const response = await GET(`${issueBaseUrl}/content-history/overview`); 136 const resp = await response.json(); 137 138 i18nTextEdited = resp.i18n.textEdited; 139 i18nTextDeleteFromHistory = resp.i18n.textDeleteFromHistory; 140 i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm; 141 i18nTextOptions = resp.i18n.textOptions; 142 143 if (resp.editedHistoryCountMap[0] && $itemIssue.length) { 144 showContentHistoryMenu(issueBaseUrl, $itemIssue, '0'); 145 } 146 for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) { 147 if (commentId === '0') continue; 148 const $itemComment = $(`#issuecomment-${commentId}`); 149 showContentHistoryMenu(issueBaseUrl, $itemComment, commentId); 150 } 151 } catch (error) { 152 console.error('Error:', error); 153 } 154 }