aboutsummaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-11-10 16:26:42 +0800
committerGitHub <noreply@github.com>2024-11-10 08:26:42 +0000
commit58c634b8549fb279aec72cecd6a48511803db067 (patch)
tree15f734f16ac5c4cf3a84301ec33dc968845f412b /web_src
parentb55a31eb6a894feb5508e350ff5e9548b2531bd6 (diff)
downloadgitea-58c634b8549fb279aec72cecd6a48511803db067.tar.gz
gitea-58c634b8549fb279aec72cecd6a48511803db067.zip
Refactor sidebar label selector (#32460)
Introduce `issueSidebarLabelsData` to handle all sidebar labels related data.
Diffstat (limited to 'web_src')
-rw-r--r--web_src/css/repo.css8
-rw-r--r--web_src/css/repo/issue-label.css5
-rw-r--r--web_src/js/features/common-page.ts4
-rw-r--r--web_src/js/features/repo-issue-sidebar-combolist.ts46
-rw-r--r--web_src/js/features/repo-issue-sidebar.md27
-rw-r--r--web_src/js/features/repo-issue-sidebar.ts20
-rw-r--r--web_src/js/features/repo-issue.ts15
-rw-r--r--web_src/js/index.ts3
-rw-r--r--web_src/js/utils/dom.ts10
9 files changed, 83 insertions, 55 deletions
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 185a5f6f55..ff8342d29a 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -50,7 +50,7 @@
width: 300px;
}
-.issue-sidebar-combo .ui.dropdown .item:not(.checked) svg.octicon-check {
+.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
visibility: hidden;
}
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
@@ -62,6 +62,8 @@
.issue-content-right .dropdown > .menu {
max-width: 270px;
min-width: 0;
+ max-height: 500px;
+ overflow-x: auto;
}
@media (max-width: 767.98px) {
@@ -110,10 +112,6 @@
left: 0;
}
-.repository .select-label .desc {
- padding-left: 23px;
-}
-
/* 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-label.css b/web_src/css/repo/issue-label.css
index 9b4b144a00..0a25d31da9 100644
--- a/web_src/css/repo/issue-label.css
+++ b/web_src/css/repo/issue-label.css
@@ -47,6 +47,7 @@
}
.archived-label-hint {
- float: right;
- margin: -12px;
+ position: absolute;
+ top: 10px;
+ right: 5px;
}
diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts
index beec92d152..56c5915b6d 100644
--- a/web_src/js/features/common-page.ts
+++ b/web_src/js/features/common-page.ts
@@ -32,13 +32,13 @@ export function initGlobalDropdown() {
const $uiDropdowns = fomanticQuery('.ui.dropdown');
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
- $uiDropdowns.filter(':not(.custom)').dropdown();
+ $uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
- $uiDropdowns.filter('.jump').dropdown({
+ $uiDropdowns.filter('.jump').dropdown('setting', {
action: 'hide',
onShow() {
// hide associated tooltip while dropdown is open
diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts
index d541615988..f408eb43ba 100644
--- a/web_src/js/features/repo-issue-sidebar-combolist.ts
+++ b/web_src/js/features/repo-issue-sidebar-combolist.ts
@@ -1,6 +1,6 @@
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {POST} from '../modules/fetch.ts';
-import {queryElemChildren, toggleElem} from '../utils/dom.ts';
+import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments
export function issueSidebarReloadConfirmDraftComment() {
@@ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) {
}
export function initIssueSidebarComboList(container: HTMLElement) {
- if (!container) return;
-
const updateUrl = container.getAttribute('data-update-url');
const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
- const initialValues = collectCheckedValues(elDropdown);
+ let initialValues = collectCheckedValues(elDropdown);
elDropdown.addEventListener('click', (e) => {
const elItem = (e.target as HTMLElement).closest('.item');
if (!elItem) return;
e.preventDefault();
- if (elItem.getAttribute('data-can-change') !== 'true') return;
- elItem.classList.toggle('checked');
+ if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
+
+ if (elItem.matches('.clear-selection')) {
+ queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
+ elComboValue.value = '';
+ return;
+ }
+
+ const scope = elItem.getAttribute('data-scope');
+ if (scope) {
+ // scoped items could only be checked one at a time
+ const elSelected = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
+ if (elSelected === elItem) {
+ elItem.classList.toggle('checked');
+ } else {
+ queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
+ elItem.classList.toggle('checked', true);
+ }
+ } else {
+ elItem.classList.toggle('checked');
+ }
elComboValue.value = collectCheckedValues(elDropdown).join(',');
});
@@ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) {
if (changed) issueSidebarReloadConfirmDraftComment();
};
- const syncList = (changedValues) => {
+ const syncUiList = (changedValues) => {
const elEmptyTip = elList.querySelector('.item.empty-list');
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
for (const value of changedValues) {
- const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${value}"]`);
+ const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
const listItem = el.cloneNode(true) as HTMLElement;
- listItem.querySelector('svg.octicon-check')?.remove();
+ queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
elList.append(listItem);
}
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
toggleElem(elEmptyTip, !hasItems);
};
- fomanticQuery(elDropdown).dropdown({
+ fomanticQuery(elDropdown).dropdown('setting', {
action: 'nothing', // do not hide the menu if user presses Enter
fullTextSearch: 'exact',
async onHide() {
+ // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
const changedValues = collectCheckedValues(elDropdown);
- if (updateUrl) {
- await updateToBackend(changedValues); // send requests to backend and reload the page
- } else {
- syncList(changedValues); // only update the list in the sidebar
- }
+ syncUiList(changedValues);
+ if (updateUrl) await updateToBackend(changedValues);
+ initialValues = changedValues;
},
});
}
diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md
new file mode 100644
index 0000000000..3022b52d05
--- /dev/null
+++ b/web_src/js/features/repo-issue-sidebar.md
@@ -0,0 +1,27 @@
+A sidebar combo (dropdown+list) is like this:
+
+```html
+<div class="issue-sidebar-combo" data-update-url="...">
+ <input class="combo-value" name="..." type="hidden" value="...">
+ <div class="ui dropdown">
+ <div class="menu">
+ <div class="item clear-selection">clear</div>
+ <div class="item" data-value="..." data-scope="...">
+ <span class="item-check-mark">...</span>
+ ...
+ </div>
+ </div>
+ </div>
+ <div class="ui list">
+ <span class="item empty-list">no item</span>
+ <span class="item">...</span>
+ </div>
+</div>
+```
+
+When the selected items change, the `combo-value` input will be updated.
+If there is `data-update-url`, it also calls backend to attach/detach the changed items.
+
+Also, the changed items will be syncronized to the `ui list` items.
+
+The items with the same data-scope only allow one selected at a time.
diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts
index 4a1ef02aab..52878848e8 100644
--- a/web_src/js/features/repo-issue-sidebar.ts
+++ b/web_src/js/features/repo-issue-sidebar.ts
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
import {updateIssuesMeta} from './repo-common.ts';
import {svg} from '../svg.ts';
import {htmlEscape} from 'escape-goat';
-import {toggleElem} from '../utils/dom.ts';
+import {queryElems, toggleElem} from '../utils/dom.ts';
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
@@ -28,7 +28,7 @@ function initBranchSelector() {
} else {
// for new issue, only update UI&form, do not send request/reload
const selectedHiddenSelector = this.getAttribute('data-id-selector');
- document.querySelector(selectedHiddenSelector).value = selectedValue;
+ document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue;
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
}
});
@@ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) {
for (const [elementId, item] of itemEntries) {
await updateIssuesMeta(
item['update-url'],
- item.action,
+ item['action'],
item['issue-id'],
elementId,
);
@@ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) {
if (scope) {
// Enable only clicked item for scoped labels
if (this.getAttribute('data-scope') !== scope) {
- return true;
+ return;
}
if (this !== clickedItem && !this.classList.contains('checked')) {
- return true;
+ return;
}
} else if (this !== clickedItem) {
// Toggle for other labels
- return true;
+ return;
}
if (this.classList.contains('checked')) {
@@ -258,13 +258,13 @@ export function initRepoIssueSidebar() {
initRepoIssueDue();
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
- initListSubmits('select-label', 'labels');
initListSubmits('select-assignees', 'assignees');
initListSubmits('select-assignees-modify', 'assignees');
+ selectItem('.select-assignee', '#assignee_id');
+
selectItem('.select-project', '#project_id');
selectItem('.select-milestone', '#milestone_id');
- selectItem('.select-assignee', '#assignee_id');
- // init the combo list: a dropdown for selecting reviewers, and a list for showing selected reviewers and related actions
- initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]'));
+ // init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
+ queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
}
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index 92916ec8d7..7457531ece 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -98,6 +98,7 @@ export function initRepoIssueSidebarList() {
});
});
+ // FIXME: it is wrong place to init ".ui.dropdown.label-filter"
$('.menu .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');
@@ -106,7 +107,6 @@ export function initRepoIssueSidebarList() {
}
}
});
- $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
}
export function initRepoIssueCommentDelete() {
@@ -652,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) {
}
}
-// This function used to show and hide archived label on issue/pr
-// page in the sidebar where we select the labels
-// If we have any archived label tagged to issue and pr. We will show that
-// archived label with checked classed otherwise we will hide it
-// with the help of this function.
-// This function runs globally.
-export function initArchivedLabelHandler() {
- if (!document.querySelector('.archived-label-hint')) return;
- for (const label of document.querySelectorAll('[data-is-archived]')) {
- toggleElem(label, label.classList.contains('checked'));
- }
-}
-
export function initRepoCommentFormAndSidebar() {
const $commentForm = $('.comment.form');
if (!$commentForm.length) return;
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 487aac97aa..eeead37333 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -30,7 +30,7 @@ import {
initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit,
- initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
+ initRepoPullRequestReview, initRepoIssueSidebarList,
} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
@@ -182,7 +182,6 @@ onDomReady(() => {
initRepoIssueContentHistory,
initRepoIssueList,
initRepoIssueSidebarList,
- initArchivedLabelHandler,
initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle,
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 79ce05a7ad..29b34dd1e3 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
import type $ from 'jquery';
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
-type ElementsCallback = (el: Element) => Promisable<any>;
+type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
@@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
return res[0];
}
-function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
+function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
if (fn) {
for (const el of elems) {
fn(el);
@@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?:
return elems;
}
-export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
const elems = Array.from(el.parentNode.children) as T[];
return applyElemsCallback<T>(elems.filter((child: Element) => {
return child !== el && child.matches(selector);
@@ -75,13 +75,13 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
}
// it works like jQuery.children: only the direct children are selected
-export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
-export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
}