From 2c71d33f8029ac92796f675690d615f8e589f4aa Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 8 Dec 2024 18:47:16 +0800 Subject: [PATCH 1/4] fix --- .eslintrc.yaml | 2 +- templates/projects/view.tmpl | 5 +- templates/repo/issue/filter_list.tmpl | 5 +- templates/repo/issue/sidebar/label_list.tmpl | 4 +- web_src/js/features/repo-issue.ts | 56 +++++++-------- web_src/js/index.ts | 3 +- web_src/js/modules/fomantic/dropdown.test.ts | 54 +++++++++++++++ web_src/js/modules/fomantic/dropdown.ts | 73 ++++++++++++++++++++ web_src/js/webcomponents/overflow-menu.ts | 2 +- 9 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 web_src/js/modules/fomantic/dropdown.test.ts diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 048ca393d1be7..04eb02363452d 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -818,7 +818,7 @@ rules: unicorn/consistent-destructuring: [2] unicorn/consistent-empty-array-spread: [2] unicorn/consistent-existence-index-check: [0] - unicorn/consistent-function-scoping: [2] + unicorn/consistent-function-scoping: [0] unicorn/custom-error-definition: [0] unicorn/empty-brace-spaces: [2] unicorn/error-message: [0] diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 71f9d059adb9b..acaf45e8d28e9 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -36,10 +36,11 @@ {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} - + {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 7335c949f4738..e686f1d60f6db 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -30,10 +30,11 @@ {{range .Labels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} -
+ {{if .IsExcluded}} {{svg "octicon-circle-slash"}} {{else if .IsSelected}} diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index fb8f1a667e75c..9dd83ba188294 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -22,7 +22,7 @@ {{range $data.RepoLabels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} {{template "repo/issue/sidebar/label_list_item" dict "Label" .}} @@ -32,7 +32,7 @@ {{range $data.OrgLabels}} {{$exclusiveScope := .ExclusiveScope}} {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}} -
+
{{end}} {{$previousExclusiveScope = $exclusiveScope}} {{template "repo/issue/sidebar/label_list_item" dict "Label" .}} diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index 477edbeb5f4c9..f5a36b7717e28 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -8,6 +8,7 @@ import {parseIssuePageInfo, toAbsoluteUrl} from '../utils.ts'; import {GET, POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {initRepoIssueSidebar} from './repo-issue-sidebar.ts'; +import {fomanticQuery} from '../modules/fomantic/base.ts'; const {appSubUrl} = window.config; @@ -31,34 +32,35 @@ export function initRepoIssueSidebarList() { if (crossRepoSearch === 'true') { issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${issuePageInfo.repoId}&type=${issuePageInfo.issueDependencySearchType}`; } - $('#new-dependency-drop-list') - .dropdown({ - apiSettings: { - url: issueSearchUrl, - onResponse(response) { - const filteredResponse = {success: true, results: []}; - const currIssueId = $('#new-dependency-drop-list').data('issue-id'); - // Parse the response from the api to work with our dropdown - $.each(response, (_i, issue) => { - // Don't list current issue in the dependency list. - if (issue.id === currIssueId) { - return; - } - filteredResponse.results.push({ - name: `
#${issue.number} ${htmlEscape(issue.title)}
+ fomanticQuery('#new-dependency-drop-list').dropdown({ + fullTextSearch: true, + apiSettings: { + url: issueSearchUrl, + onResponse(response) { + const filteredResponse = {success: true, results: []}; + const currIssueId = $('#new-dependency-drop-list').data('issue-id'); + // Parse the response from the api to work with our dropdown + $.each(response, (_i, issue) => { + // Don't list current issue in the dependency list. + if (issue.id === currIssueId) { + return; + } + filteredResponse.results.push({ + name: `
#${issue.number} ${htmlEscape(issue.title)}
${htmlEscape(issue.repository.full_name)}
`, - value: issue.id, - }); + value: issue.id, }); - return filteredResponse; - }, - cache: false, + }); + return filteredResponse; }, + cache: false, + }, + }); +} - fullTextSearch: true, - }); - - $('.menu a.label-filter-item').each(function () { +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(); @@ -66,11 +68,9 @@ export function initRepoIssueSidebarList() { } }); }); - - // FIXME: it is wrong place to init ".ui.dropdown.label-filter" - $('.menu .ui.dropdown.label-filter').on('keydown', (e) => { + $('.ui.dropdown.label-filter').on('keydown', (e) => { if (e.altKey && e.key === 'Enter') { - const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected'); + const selectedItem = document.querySelector('.ui.dropdown.label-filter .menu .item.selected'); if (selectedItem) { excludeLabel(selectedItem); } diff --git a/web_src/js/index.ts b/web_src/js/index.ts index f93c3495af4c7..2964ef557200b 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -29,7 +29,7 @@ import { initRepoIssueWipTitle, initRepoPullRequestMergeInstruction, initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, initRepoIssueSidebarList, + initRepoPullRequestReview, initRepoIssueSidebarList, initRepoIssueLabelFilter, } from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; @@ -181,6 +181,7 @@ onDomReady(() => { initRepoGraphGit, initRepoIssueContentHistory, initRepoIssueList, + initRepoIssueLabelFilter, initRepoIssueSidebarList, initRepoIssueReferenceRepositorySearch, initRepoIssueWipTitle, diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts new file mode 100644 index 0000000000000..794763c804768 --- /dev/null +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -0,0 +1,54 @@ +import {createElementFromHTML} from '../../utils/dom.ts'; +import {hideScopedEmptyDividers} from './dropdown.ts'; + +test('hideScopedEmptyDividers-simple', () => { + const container = createElementFromHTML(`
+
+
a
+
+
+
b
+
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` + +
a
+ +
+
b
+ +`); +}); + +test('hideScopedEmptyDividers-hidden1', () => { + const container = createElementFromHTML(`
+
a
+
+
b
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +
a
+ +
b
+`); +}); + +test('hideScopedEmptyDividers-hidden2', () => { + const container = createElementFromHTML(`
+
a
+
+
b
+
+
c
+
`); + hideScopedEmptyDividers(container); + expect(container.innerHTML).toEqual(` +
a
+ +
b
+ +
c
+`); +}); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index d8fb4d6e6e41a..296f6e0e06280 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -59,6 +59,12 @@ function updateSelectionLabel(label: HTMLElement) { } } +function processMenuItems($dropdown, dropdownCall) { + const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; + const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); + if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); +} + // delegate the dropdown's template functions and callback functions to add aria attributes. function delegateOne($dropdown: any) { const dropdownCall = fomanticDropdownFn.bind($dropdown); @@ -72,6 +78,18 @@ function delegateOne($dropdown: any) { // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') }); + const oldFilterItems = dropdownCall('internal', 'filterItems'); + dropdownCall('internal', 'filterItems', function () { + oldFilterItems.call(this); + processMenuItems($dropdown, dropdownCall); + }); + + const oldShow = dropdownCall('internal', 'show'); + dropdownCall('internal', 'show', function (...args) { + oldShow.call(this, ...args); + processMenuItems($dropdown, dropdownCall); + }); + // the "template" functions are used for dynamic creation (eg: AJAX) const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()}; const dropdownTemplatesMenuOld = dropdownTemplates.menu; @@ -271,3 +289,58 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT ignoreClickPreEvents = ignoreClickPreVisible = 0; }, true); } + +export function hideScopedEmptyDividers(container: Element) { + const visibleItems: Element[] = []; + const curScopeVisibleItems: Element[] = []; + let curScope = '', lastVisibleScope = ''; + const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope'); + const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items + + const handleScopeSwitch = (itemScope: string) => { + if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) { + hideDivider(curScopeVisibleItems[0]); + } else if (curScopeVisibleItems.length) { + if (isScopedDivider(curScopeVisibleItems[0]) && lastVisibleScope === curScope) { + hideDivider(curScopeVisibleItems[0]); + curScopeVisibleItems.shift(); + } + visibleItems.push(...curScopeVisibleItems); + lastVisibleScope = curScope; + } + curScope = itemScope; + curScopeVisibleItems.length = 0; + }; + + // hide the scope dividers if the scope items are empty + for (const item of container.children) { + const itemScope = item.getAttribute('data-scope') || ''; + if (itemScope !== curScope) { + handleScopeSwitch(itemScope); + } + if (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) { + curScopeVisibleItems.push(item as HTMLElement); + } + } + handleScopeSwitch(''); + + // hide all leading and trailing dividers + while (visibleItems.length) { + if (!visibleItems[0].matches('.divider')) break; + hideDivider(visibleItems[0]); + visibleItems.shift(); + } + while (visibleItems.length) { + if (!visibleItems[visibleItems.length - 1].matches('.divider')) break; + hideDivider(visibleItems[visibleItems.length - 1]); + visibleItems.pop(); + } + // hide all duplicate dividers + for (let i = 0; i < visibleItems.length; i++) { + const item = visibleItems[i]; + if (!item.matches('.divider')) continue; + if (i === 0 || i === visibleItems.length - 1 || item.nextElementSibling?.matches('.divider')) { + hideDivider(item); + } + } +} diff --git a/web_src/js/webcomponents/overflow-menu.ts b/web_src/js/webcomponents/overflow-menu.ts index 777d7dc65dd39..4e729a268a02f 100644 --- a/web_src/js/webcomponents/overflow-menu.ts +++ b/web_src/js/webcomponents/overflow-menu.ts @@ -12,7 +12,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { mutationObserver: MutationObserver; lastWidth: number; - updateItems = throttle(100, () => { // eslint-disable-line unicorn/consistent-function-scoping -- https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2088 + updateItems = throttle(100, () => { if (!this.tippyContent) { const div = document.createElement('div'); div.classList.add('tippy-target'); From 25afea4d1a5543b657651227587537199987ae98 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 9 Dec 2024 08:33:56 +0800 Subject: [PATCH 2/4] add comments --- web_src/js/modules/fomantic/dropdown.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 296f6e0e06280..f29bb95c0e13d 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -290,6 +290,15 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT }, true); } +// Although Fomantic Dropdown supports "hideDividers", it doesn't really work with our "scoped dividers" +// At the moment, "label dropdown items" use scopes, a sample case is: +// * a-label +// * divider +// * scope/1 +// * scope/2 +// * divider +// * z-label +// when the "scope/*" are filtered out, we'd like to see "a-label" and "z-label" without the divider. export function hideScopedEmptyDividers(container: Element) { const visibleItems: Element[] = []; const curScopeVisibleItems: Element[] = []; From a437d45c2e133a08900e89baa8fb37a1c1a8396e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 9 Dec 2024 08:58:35 +0800 Subject: [PATCH 3/4] fine tune --- web_src/js/modules/fomantic/dropdown.test.ts | 2 ++ web_src/js/modules/fomantic/dropdown.ts | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web_src/js/modules/fomantic/dropdown.test.ts b/web_src/js/modules/fomantic/dropdown.test.ts index 794763c804768..587e0bca7c63f 100644 --- a/web_src/js/modules/fomantic/dropdown.test.ts +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -7,6 +7,7 @@ test('hideScopedEmptyDividers-simple', () => {
a
+
b
`); @@ -15,6 +16,7 @@ test('hideScopedEmptyDividers-simple', () => {
a
+
b
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index f29bb95c0e13d..831fd43555f98 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -302,7 +302,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT export function hideScopedEmptyDividers(container: Element) { const visibleItems: Element[] = []; const curScopeVisibleItems: Element[] = []; - let curScope = '', lastVisibleScope = ''; + let curScope: string = '', lastVisibleScope: string = ''; const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope'); const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items @@ -344,12 +344,10 @@ export function hideScopedEmptyDividers(container: Element) { hideDivider(visibleItems[visibleItems.length - 1]); visibleItems.pop(); } - // hide all duplicate dividers - for (let i = 0; i < visibleItems.length; i++) { - const item = visibleItems[i]; + // hide all duplicate dividers, hide current divider if next sibling is still divider + // no need to update "visibleItems" array since this is the last loop + for (const item of visibleItems) { if (!item.matches('.divider')) continue; - if (i === 0 || i === visibleItems.length - 1 || item.nextElementSibling?.matches('.divider')) { - hideDivider(item); - } + if (item.nextElementSibling?.matches('.divider')) hideDivider(item); } } From ed0167a9476a41a125596f577dd6754f6b00d5fb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 9 Dec 2024 10:02:35 +0800 Subject: [PATCH 4/4] fix type --- web_src/js/modules/fomantic/dropdown.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 831fd43555f98..6d0f12cb43887 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -79,13 +79,13 @@ function delegateOne($dropdown: any) { dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') }); const oldFilterItems = dropdownCall('internal', 'filterItems'); - dropdownCall('internal', 'filterItems', function () { - oldFilterItems.call(this); + dropdownCall('internal', 'filterItems', function (...args: any[]) { + oldFilterItems.call(this, ...args); processMenuItems($dropdown, dropdownCall); }); const oldShow = dropdownCall('internal', 'show'); - dropdownCall('internal', 'show', function (...args) { + dropdownCall('internal', 'show', function (...args: any[]) { oldShow.call(this, ...args); processMenuItems($dropdown, dropdownCall); });