mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Refactor issue filter (labels, poster, assignee) (#32771)
Rewrite a lot of legacy strange code, remove duplicate code, remove jquery, and make these filters reusable. Let's forget the old code, new code affects: * issue list open/close switch * issue list filter (label, author, assignee) * milestone list open/close switch * milestone issue list filter (label, author, assignee) * project view (label, assignee)
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| import {updateIssuesMeta} from './repo-common.ts'; | ||||
| import {toggleElem, hideElem, isElemHidden, queryElems} from '../utils/dom.ts'; | ||||
| import {toggleElem, isElemHidden, queryElems} from '../utils/dom.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {confirmModal} from './comp/ConfirmModal.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| @@ -95,34 +95,51 @@ function initRepoIssueListCheckboxes() { | ||||
| function initDropdownUserRemoteSearch(el: Element) { | ||||
|   let searchUrl = el.getAttribute('data-search-url'); | ||||
|   const actionJumpUrl = el.getAttribute('data-action-jump-url'); | ||||
|   const selectedUserId = el.getAttribute('data-selected-user-id'); | ||||
|   const selectedUserId = parseInt(el.getAttribute('data-selected-user-id')); | ||||
|   let selectedUsername = ''; | ||||
|   if (!searchUrl.includes('?')) searchUrl += '?'; | ||||
|   const $searchDropdown = fomanticQuery(el); | ||||
|   const elSearchInput = el.querySelector<HTMLInputElement>('.ui.search input'); | ||||
|   const elItemFromInput = el.querySelector('.menu > .item-from-input'); | ||||
|  | ||||
|   $searchDropdown.dropdown('setting', { | ||||
|     fullTextSearch: true, | ||||
|     selectOnKeydown: false, | ||||
|     apiSettings: { | ||||
|     action: (_text, value) => { | ||||
|       window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   type ProcessedResult = {value: string, name: string}; | ||||
|   const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items | ||||
|   const syncItemFromInput = () => { | ||||
|     elItemFromInput.setAttribute('data-value', elSearchInput.value); | ||||
|     elItemFromInput.textContent = elSearchInput.value; | ||||
|     toggleElem(elItemFromInput, !processedResults.length); | ||||
|   }; | ||||
|  | ||||
|   if (!searchUrl) { | ||||
|     elSearchInput.addEventListener('input', syncItemFromInput); | ||||
|   } else { | ||||
|     $searchDropdown.dropdown('setting', 'apiSettings', { | ||||
|       cache: false, | ||||
|       url: `${searchUrl}&q={query}`, | ||||
|       onResponse(resp) { | ||||
|         // the content is provided by backend IssuePosters handler | ||||
|         const processedResults = []; // to be used by dropdown to generate menu items | ||||
|         processedResults.length = 0; | ||||
|         for (const item of resp.results) { | ||||
|           let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; | ||||
|           if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`; | ||||
|           if (selectedUserId === item.user_id) selectedUsername = item.username; | ||||
|           processedResults.push({value: item.username, name: html}); | ||||
|         } | ||||
|         resp.results = processedResults; | ||||
|         syncItemFromInput(); | ||||
|         return resp; | ||||
|       }, | ||||
|     }, | ||||
|     action: (_text, value) => { | ||||
|       window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); | ||||
|     }, | ||||
|     onShow: () => { | ||||
|       $searchDropdown.dropdown('filter', ' '); // trigger a search on first show | ||||
|     }, | ||||
|   }); | ||||
|     }); | ||||
|     $searchDropdown.dropdown('setting', 'onShow', () => $searchDropdown.dropdown('filter', ' ')); // trigger a search on first show | ||||
|   } | ||||
|  | ||||
|   // we want to generate the dropdown menu items by ourselves, replace its internal setup functions | ||||
|   const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; | ||||
| @@ -151,7 +168,7 @@ function initDropdownUserRemoteSearch(el: Element) { | ||||
|       for (const el of menu.querySelectorAll('.item.active, .item.selected')) { | ||||
|         el.classList.remove('active', 'selected'); | ||||
|       } | ||||
|       menu.querySelector(`.item[data-value="${selectedUserId}"]`)?.classList.add('selected'); | ||||
|       menu.querySelector(`.item[data-value="${CSS.escape(selectedUsername)}"]`)?.classList.add('selected'); | ||||
|     }, 0); | ||||
|   }; | ||||
| } | ||||
| @@ -203,44 +220,9 @@ async function initIssuePinSort() { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function initArchivedLabelFilter() { | ||||
|   const archivedLabelEl = document.querySelector<HTMLInputElement>('#archived-filter-checkbox'); | ||||
|   if (!archivedLabelEl) return; | ||||
|  | ||||
|   const url = new URL(window.location.href); | ||||
|   const archivedLabels = document.querySelectorAll('[data-is-archived]'); | ||||
|  | ||||
|   if (!archivedLabels.length) { | ||||
|     hideElem('.archived-label-filter'); | ||||
|     return; | ||||
|   } | ||||
|   const selectedLabels = (url.searchParams.get('labels') || '') | ||||
|     .split(',') | ||||
|     .map((id) => parseInt(id) < 0 ? `${~id + 1}` : id); // selectedLabels contains -ve ids, which are excluded so convert any -ve value id to +ve | ||||
|  | ||||
|   const archivedElToggle = () => { | ||||
|     for (const label of archivedLabels) { | ||||
|       const id = label.getAttribute('data-label-id'); | ||||
|       toggleElem(label, archivedLabelEl.checked || selectedLabels.includes(id)); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   archivedElToggle(); | ||||
|   archivedLabelEl.addEventListener('change', () => { | ||||
|     archivedElToggle(); | ||||
|     if (archivedLabelEl.checked) { | ||||
|       url.searchParams.set('archived', 'true'); | ||||
|     } else { | ||||
|       url.searchParams.delete('archived'); | ||||
|     } | ||||
|     window.location.href = url.href; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueList() { | ||||
|   if (!document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) return; | ||||
|   initRepoIssueListCheckboxes(); | ||||
|   queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el)); | ||||
|   initIssuePinSort(); | ||||
|   initArchivedLabelFilter(); | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,14 @@ | ||||
| import $ from 'jquery'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts'; | ||||
| import {addDelegatedEventListener, createElementFromHTML, hideElem, showElem, toggleElem} from '../utils/dom.ts'; | ||||
| import { | ||||
|   addDelegatedEventListener, | ||||
|   createElementFromHTML, | ||||
|   hideElem, | ||||
|   queryElems, | ||||
|   showElem, | ||||
|   toggleElem, | ||||
| } from '../utils/dom.ts'; | ||||
| import {setFileFolding} from './file-fold.ts'; | ||||
| import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts'; | ||||
| @@ -12,19 +19,6 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||
|  | ||||
| const {appSubUrl} = window.config; | ||||
|  | ||||
| /** | ||||
|  * @param {HTMLElement} item | ||||
|  */ | ||||
| function excludeLabel(item) { | ||||
|   const href = item.getAttribute('href'); | ||||
|   const id = item.getAttribute('data-label-id'); | ||||
|  | ||||
|   const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`; | ||||
|   const newStr = 'labels=$1-$2$3&'; | ||||
|  | ||||
|   window.location.assign(href.replace(new RegExp(regStr), newStr)); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueSidebarList() { | ||||
|   const issuePageInfo = parseIssuePageInfo(); | ||||
|   const crossRepoSearch = $('#crossRepoSearch').val(); | ||||
| @@ -58,24 +52,74 @@ export function initRepoIssueSidebarList() { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueLabelFilter() { | ||||
|   // the "label-filter" is used in 2 templates: projects/view, issue/filter_list (issue list page including the milestone page) | ||||
|   $('.ui.dropdown.label-filter a.label-filter-item').each(function () { | ||||
|     $(this).on('click', function (e) { | ||||
|       if (e.altKey) { | ||||
|         e.preventDefault(); | ||||
|         excludeLabel(this); | ||||
|       } | ||||
| function initRepoIssueLabelFilter(elDropdown: Element) { | ||||
|   const url = new URL(window.location.href); | ||||
|   const showArchivedLabels = url.searchParams.get('archived_labels') === 'true'; | ||||
|   const queryLabels = url.searchParams.get('labels') || ''; | ||||
|   const selectedLabelIds = new Set<string>(); | ||||
|   for (const id of queryLabels ? queryLabels.split(',') : []) { | ||||
|     selectedLabelIds.add(`${Math.abs(parseInt(id))}`); // "labels" contains negative ids, which are excluded | ||||
|   } | ||||
|  | ||||
|   const excludeLabel = (e: MouseEvent|KeyboardEvent, item: Element) => { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|     const labelId = item.getAttribute('data-label-id'); | ||||
|     let labelIds: string[] = queryLabels ? queryLabels.split(',') : []; | ||||
|     labelIds = labelIds.filter((id) => Math.abs(parseInt(id)) !== Math.abs(parseInt(labelId))); | ||||
|     labelIds.push(`-${labelId}`); | ||||
|     url.searchParams.set('labels', labelIds.join(',')); | ||||
|     window.location.assign(url); | ||||
|   }; | ||||
|  | ||||
|   // alt(or option) + click to exclude label | ||||
|   queryElems(elDropdown, '.label-filter-query-item', (el) => { | ||||
|     el.addEventListener('click', (e: MouseEvent) => { | ||||
|       if (e.altKey) excludeLabel(e, el); | ||||
|     }); | ||||
|   }); | ||||
|   $('.ui.dropdown.label-filter').on('keydown', (e) => { | ||||
|   // alt(or option) + enter to exclude selected label | ||||
|   elDropdown.addEventListener('keydown', (e: KeyboardEvent) => { | ||||
|     if (e.altKey && e.key === 'Enter') { | ||||
|       const selectedItem = document.querySelector('.ui.dropdown.label-filter .menu .item.selected'); | ||||
|       if (selectedItem) { | ||||
|         excludeLabel(selectedItem); | ||||
|       } | ||||
|       const selectedItem = elDropdown.querySelector('.label-filter-query-item.selected'); | ||||
|       if (selectedItem) excludeLabel(e, selectedItem); | ||||
|     } | ||||
|   }); | ||||
|   // no "labels" query parameter means "all issues" | ||||
|   elDropdown.querySelector('.label-filter-query-default').classList.toggle('selected', queryLabels === ''); | ||||
|   // "labels=0" query parameter means "issues without label" | ||||
|   elDropdown.querySelector('.label-filter-query-not-set').classList.toggle('selected', queryLabels === '0'); | ||||
|  | ||||
|   // prepare to process "archived" labels | ||||
|   const elShowArchivedLabel = elDropdown.querySelector('.label-filter-archived-toggle'); | ||||
|   if (!elShowArchivedLabel) return; | ||||
|   const elShowArchivedInput = elShowArchivedLabel.querySelector<HTMLInputElement>('input'); | ||||
|   elShowArchivedInput.checked = showArchivedLabels; | ||||
|   const archivedLabels = elDropdown.querySelectorAll('.item[data-is-archived]'); | ||||
|   // if no archived labels, hide the toggle and return | ||||
|   if (!archivedLabels.length) { | ||||
|     hideElem(elShowArchivedLabel); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // show the archived labels if the toggle is checked or the label is selected | ||||
|   for (const label of archivedLabels) { | ||||
|     toggleElem(label, showArchivedLabels || selectedLabelIds.has(label.getAttribute('data-label-id'))); | ||||
|   } | ||||
|   // update the url when the toggle is changed and reload | ||||
|   elShowArchivedInput.addEventListener('input', () => { | ||||
|     if (elShowArchivedInput.checked) { | ||||
|       url.searchParams.set('archived_labels', 'true'); | ||||
|     } else { | ||||
|       url.searchParams.delete('archived_labels'); | ||||
|     } | ||||
|     window.location.assign(url); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueFilterItemLabel() { | ||||
|   // the "label-filter" is used in 2 templates: projects/view, issue/filter_list (issue list page including the milestone page) | ||||
|   queryElems(document, '.ui.dropdown.label-filter', initRepoIssueLabelFilter); | ||||
| } | ||||
|  | ||||
| export function initRepoIssueCommentDelete() { | ||||
|   | ||||
| @@ -29,7 +29,7 @@ import { | ||||
|   initRepoIssueWipTitle, | ||||
|   initRepoPullRequestMergeInstruction, | ||||
|   initRepoPullRequestAllowMaintainerEdit, | ||||
|   initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueLabelFilter, | ||||
|   initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueFilterItemLabel, | ||||
| } from './features/repo-issue.ts'; | ||||
| import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; | ||||
| import {initRepoTopicBar} from './features/repo-home.ts'; | ||||
| @@ -181,7 +181,7 @@ onDomReady(() => { | ||||
|     initRepoGraphGit, | ||||
|     initRepoIssueContentHistory, | ||||
|     initRepoIssueList, | ||||
|     initRepoIssueLabelFilter, | ||||
|     initRepoIssueFilterItemLabel, | ||||
|     initRepoIssueSidebarList, | ||||
|     initRepoIssueReferenceRepositorySearch, | ||||
|     initRepoIssueWipTitle, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user