From 2d13eafd69b368bc75faa25d7ecfbda98a0792f8 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Mon, 9 Dec 2024 16:30:16 +0900 Subject: [PATCH 1/2] Remove unnecessary border in repo home page sidebar (#32767) --- web_src/css/repo/home.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index fd8fac27e27..ca5b432804f 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -20,7 +20,7 @@ grid-row: 2; padding-left: 1em; } -.repo-home-sidebar-bottom > :first-child { +.repo-home-sidebar-bottom .flex-list > :first-child { border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */ } @@ -43,7 +43,7 @@ grid-row: 3; padding-left: 0; } - .repo-home-sidebar-bottom > :first-child { + .repo-home-sidebar-bottom .flex-list > :first-child { border-top: 0; } } From 5675efb3e015fb8d87d86d1b79de2200f7dfb9af Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 9 Dec 2024 15:54:59 +0800 Subject: [PATCH 2/2] Fix duplicate dropdown dividers (#32760) Fix #27466 The problem is that any item in the menu could be hidden, pure CSS won't work, and dropdown's builtin "hideDividers" doesn't work with our "scope dividers". The newly introduced "archived" label makes the dividers regression more. --- .eslintrc.yaml | 2 +- modules/templates/helper.go | 8 +- 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 | 56 ++++++++++++++ web_src/js/modules/fomantic/dropdown.ts | 80 ++++++++++++++++++++ web_src/js/webcomponents/overflow-menu.ts | 2 +- 10 files changed, 179 insertions(+), 42 deletions(-) create mode 100644 web_src/js/modules/fomantic/dropdown.test.ts diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 048ca393d1b..04eb0236345 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/modules/templates/helper.go b/modules/templates/helper.go index fdfb21925ab..e2628920697 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -294,9 +294,7 @@ func timeEstimateString(timeSec any) string { return util.TimeEstimateString(v) } -type QueryString string - -func queryBuild(a ...any) QueryString { +func queryBuild(a ...any) template.URL { var s string if len(a)%2 == 1 { if v, ok := a[0].(string); ok { @@ -304,7 +302,7 @@ func queryBuild(a ...any) QueryString { panic("queryBuild: invalid argument") } s = v - } else if v, ok := a[0].(QueryString); ok { + } else if v, ok := a[0].(template.URL); ok { s = string(v) } else { panic("queryBuild: invalid argument") @@ -356,7 +354,7 @@ func queryBuild(a ...any) QueryString { if s != "" && s != "&" && s[len(s)-1] == '&' { s = s[:len(s)-1] } - return QueryString(s) + return template.URL(s) } func panicIfDevOrTesting() { diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 71f9d059adb..acaf45e8d28 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 7335c949f47..e686f1d60f6 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 fb8f1a667e7..9dd83ba1882 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 477edbeb5f4..f5a36b7717e 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 f93c3495af4..2964ef55720 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 00000000000..587e0bca7c6 --- /dev/null +++ b/web_src/js/modules/fomantic/dropdown.test.ts @@ -0,0 +1,56 @@ +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 d8fb4d6e6e4..6d0f12cb438 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 (...args: any[]) { + oldFilterItems.call(this, ...args); + processMenuItems($dropdown, dropdownCall); + }); + + const oldShow = dropdownCall('internal', 'show'); + dropdownCall('internal', 'show', function (...args: any[]) { + 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,65 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT ignoreClickPreEvents = ignoreClickPreVisible = 0; }, 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[] = []; + 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 + + 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, 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 (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 777d7dc65dd..4e729a268a0 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');