diff options
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/css/base.css | 5 | ||||
-rw-r--r-- | web_src/css/repo.css | 36 | ||||
-rw-r--r-- | web_src/css/repo/issue-list.css | 6 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-list.ts | 78 | ||||
-rw-r--r-- | web_src/js/features/repo-issue.ts | 98 | ||||
-rw-r--r-- | web_src/js/index.ts | 4 |
6 files changed, 126 insertions, 101 deletions
diff --git a/web_src/css/base.css b/web_src/css/base.css index 8f5ef51c4a..04f3678f3a 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -1390,8 +1390,9 @@ table th[data-sortt-desc] .svg { min-width: 0; } -/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content */ -.ui.dropdown .menu.flex-items-menu > .item { +/* to override Fomantic's default display: block for ".menu .item", and use a slightly larger gap for menu item content +the "!important" is necessary to override Fomantic UI menu item styles, meanwhile we should keep the "hidden" items still hidden */ +.ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { display: flex !important; align-items: center; gap: .5rem; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 14bdc43474..9e1def87a7 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -74,24 +74,6 @@ } } -.repository .filter.menu.labels .label-filter .menu .info { - display: inline-block; - padding: 0.5rem 0; - font-size: 12px; - width: 100%; - white-space: nowrap; - margin-left: 10px; - margin-right: 8px; - text-align: left; -} - -.repository .filter.menu.labels .label-filter .menu .info code { - border: 1px solid var(--color-secondary); - border-radius: var(--border-radius); - padding: 1px 2px; - font-size: 11px; -} - /* make all issue filter dropdown menus popup leftward, to avoid go out the viewport (right side) */ .repository .filter.menu .ui.dropdown .menu { max-height: 500px; @@ -108,6 +90,24 @@ left: 0; } +.repository .filter.menu .ui.dropdown.label-filter .menu .info { + display: inline-block; + padding: 0.5rem 0; + font-size: 12px; + width: 100%; + white-space: nowrap; + margin-left: 10px; + margin-right: 8px; + text-align: left; +} + +.repository .filter.menu .ui.dropdown.label-filter .menu .info code { + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + padding: 1px 2px; + font-size: 11px; +} + /* For the secondary pointing menu, respect its own border-bottom */ /* style reference: https://semantic-ui.com/collections/menu.html#pointing */ .repository .ui.tabs.container .ui.menu:not(.secondary.pointing) { diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 1e0f82ce27..4fafc7d6f8 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -68,10 +68,8 @@ background-color: var(--color-secondary-dark-4); } -.archived-label-filter { - margin-left: 10px; +.label-filter-archived-toggle { + margin: 8px 10px; font-size: 12px; - display: flex !important; - margin-bottom: 8px; min-width: fit-content; } diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 48e22ba3c9..a0550837ec 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -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(); } diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index f5a36b7717..e4f9ce4cde 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -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,26 +52,76 @@ 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() { // Delete comment document.addEventListener('click', async (e) => { diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2964ef5572..51d8c96fbd 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -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, |