code.gitea.io/gitea@v1.21.7/web_src/js/features/repo-issue-list.js (about) 1 import $ from 'jquery'; 2 import {updateIssuesMeta} from './repo-issue.js'; 3 import {toggleElem, hideElem} from '../utils/dom.js'; 4 import {htmlEscape} from 'escape-goat'; 5 import {confirmModal} from './comp/ConfirmModal.js'; 6 import {showErrorToast} from '../modules/toast.js'; 7 import {createSortable} from '../modules/sortable.js'; 8 import {DELETE, POST} from '../modules/fetch.js'; 9 10 function initRepoIssueListCheckboxes() { 11 const $issueSelectAll = $('.issue-checkbox-all'); 12 const $issueCheckboxes = $('.issue-checkbox'); 13 14 const syncIssueSelectionState = () => { 15 const $checked = $issueCheckboxes.filter(':checked'); 16 const anyChecked = $checked.length !== 0; 17 const allChecked = anyChecked && $checked.length === $issueCheckboxes.length; 18 19 if (allChecked) { 20 $issueSelectAll.prop({'checked': true, 'indeterminate': false}); 21 } else if (anyChecked) { 22 $issueSelectAll.prop({'checked': false, 'indeterminate': true}); 23 } else { 24 $issueSelectAll.prop({'checked': false, 'indeterminate': false}); 25 } 26 // if any issue is selected, show the action panel, otherwise show the filter panel 27 toggleElem($('#issue-filters'), !anyChecked); 28 toggleElem($('#issue-actions'), anyChecked); 29 // there are two panels but only one select-all checkbox, so move the checkbox to the visible panel 30 $('#issue-filters, #issue-actions').filter(':visible').find('.issue-list-toolbar-left').prepend($issueSelectAll); 31 }; 32 33 $issueCheckboxes.on('change', syncIssueSelectionState); 34 35 $issueSelectAll.on('change', () => { 36 $issueCheckboxes.prop('checked', $issueSelectAll.is(':checked')); 37 syncIssueSelectionState(); 38 }); 39 40 $('.issue-action').on('click', async function (e) { 41 e.preventDefault(); 42 43 const url = this.getAttribute('data-url'); 44 let action = this.getAttribute('data-action'); 45 let elementId = this.getAttribute('data-element-id'); 46 let issueIDs = []; 47 for (const el of document.querySelectorAll('.issue-checkbox:checked')) { 48 issueIDs.push(el.getAttribute('data-issue-id')); 49 } 50 issueIDs = issueIDs.join(','); 51 if (!issueIDs) return; 52 53 // for assignee 54 if (elementId === '0' && url.endsWith('/assignee')) { 55 elementId = ''; 56 action = 'clear'; 57 } 58 59 // for toggle 60 if (action === 'toggle' && e.altKey) { 61 action = 'toggle-alt'; 62 } 63 64 // for delete 65 if (action === 'delete') { 66 const confirmText = e.target.getAttribute('data-action-delete-confirm'); 67 if (!await confirmModal({content: confirmText, buttonColor: 'orange'})) { 68 return; 69 } 70 } 71 72 updateIssuesMeta( 73 url, 74 action, 75 issueIDs, 76 elementId, 77 ).then(() => { 78 window.location.reload(); 79 }).catch((reason) => { 80 showErrorToast(reason.responseJSON.error); 81 }); 82 }); 83 } 84 85 function initRepoIssueListAuthorDropdown() { 86 const $searchDropdown = $('.user-remote-search'); 87 if (!$searchDropdown.length) return; 88 89 let searchUrl = $searchDropdown.attr('data-search-url'); 90 const actionJumpUrl = $searchDropdown.attr('data-action-jump-url'); 91 const selectedUserId = $searchDropdown.attr('data-selected-user-id'); 92 if (!searchUrl.includes('?')) searchUrl += '?'; 93 94 $searchDropdown.dropdown('setting', { 95 fullTextSearch: true, 96 selectOnKeydown: false, 97 apiSettings: { 98 cache: false, 99 url: `${searchUrl}&q={query}`, 100 onResponse(resp) { 101 // the content is provided by backend IssuePosters handler 102 const processedResults = []; // to be used by dropdown to generate menu items 103 for (const item of resp.results) { 104 let html = `<img class="ui avatar gt-vm" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; 105 if (item.full_name) html += `<span class="search-fullname gt-ml-3">${htmlEscape(item.full_name)}</span>`; 106 processedResults.push({value: item.user_id, name: html}); 107 } 108 resp.results = processedResults; 109 return resp; 110 }, 111 }, 112 action: (_text, value) => { 113 window.location.href = actionJumpUrl.replace('{user_id}', encodeURIComponent(value)); 114 }, 115 onShow: () => { 116 $searchDropdown.dropdown('filter', ' '); // trigger a search on first show 117 }, 118 }); 119 120 // we want to generate the dropdown menu items by ourselves, replace its internal setup functions 121 const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; 122 const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); 123 $searchDropdown.dropdown('internal', 'setup', dropdownSetup); 124 dropdownSetup.menu = function (values) { 125 const $menu = $searchDropdown.find('> .menu'); 126 $menu.find('> .dynamic-item').remove(); // remove old dynamic items 127 128 const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className')); 129 if (newMenuHtml) { 130 const $newMenuItems = $(newMenuHtml); 131 $newMenuItems.addClass('dynamic-item'); 132 $menu.append('<div class="divider dynamic-item"></div>', ...$newMenuItems); 133 } 134 $searchDropdown.dropdown('refresh'); 135 // defer our selection to the next tick, because dropdown will set the selection item after this `menu` function 136 setTimeout(() => { 137 $menu.find('.item.active, .item.selected').removeClass('active selected'); 138 $menu.find(`.item[data-value="${selectedUserId}"]`).addClass('selected'); 139 }, 0); 140 }; 141 } 142 143 function initPinRemoveButton() { 144 for (const button of document.getElementsByClassName('issue-card-unpin')) { 145 button.addEventListener('click', async (event) => { 146 const el = event.currentTarget; 147 const id = Number(el.getAttribute('data-issue-id')); 148 149 // Send the unpin request 150 const response = await DELETE(el.getAttribute('data-unpin-url')); 151 if (response.ok) { 152 // Delete the tooltip 153 el._tippy.destroy(); 154 // Remove the Card 155 el.closest(`div.issue-card[data-issue-id="${id}"]`).remove(); 156 } 157 }); 158 } 159 } 160 161 async function pinMoveEnd(e) { 162 const url = e.item.getAttribute('data-move-url'); 163 const id = Number(e.item.getAttribute('data-issue-id')); 164 await POST(url, {data: {id, position: e.newIndex + 1}}); 165 } 166 167 async function initIssuePinSort() { 168 const pinDiv = document.getElementById('issue-pins'); 169 170 if (pinDiv === null) return; 171 172 // If the User is not a Repo Admin, we don't need to proceed 173 if (!pinDiv.hasAttribute('data-is-repo-admin')) return; 174 175 initPinRemoveButton(); 176 177 // If only one issue pinned, we don't need to make this Sortable 178 if (pinDiv.children.length < 2) return; 179 180 createSortable(pinDiv, { 181 group: 'shared', 182 animation: 150, 183 ghostClass: 'card-ghost', 184 onEnd: pinMoveEnd, 185 }); 186 } 187 188 function initArchivedLabelFilter() { 189 const archivedLabelEl = document.querySelector('#archived-filter-checkbox'); 190 if (!archivedLabelEl) { 191 return; 192 } 193 194 const url = new URL(window.location.href); 195 const archivedLabels = document.querySelectorAll('[data-is-archived]'); 196 197 if (!archivedLabels.length) { 198 hideElem('.archived-label-filter'); 199 return; 200 } 201 const selectedLabels = (url.searchParams.get('labels') || '') 202 .split(',') 203 .map((id) => id < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve 204 205 const archivedElToggle = () => { 206 for (const label of archivedLabels) { 207 const id = label.getAttribute('data-label-id'); 208 toggleElem(label, archivedLabelEl.checked || selectedLabels.includes(id)); 209 } 210 }; 211 212 archivedElToggle(); 213 archivedLabelEl.addEventListener('change', () => { 214 archivedElToggle(); 215 if (archivedLabelEl.checked) { 216 url.searchParams.set('archived', 'true'); 217 } else { 218 url.searchParams.delete('archived'); 219 } 220 window.location.href = url.href; 221 }); 222 } 223 224 export function initRepoIssueList() { 225 if (!document.querySelectorAll('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list').length) return; 226 initRepoIssueListCheckboxes(); 227 initRepoIssueListAuthorDropdown(); 228 initIssuePinSort(); 229 initArchivedLabelFilter(); 230 }