mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-16 01:43:24 +09:00
feat(repo): add quick repository switcher to repo header (#38188)
Add a GitHub-style quick repo switcher: a caret next to the owner/repo breadcrumb opens a dropdown that lists and searches the current owner's repositories and navigates to the selected one. The current repository is marked with a check, and private/fork repos show an icon. Also, fix various bugs in fomtantic dropdown remote query ## Screenshots <img width="505" height="198" alt="image" src="https://github.com/user-attachments/assets/9f673d1b-fe60-41f0-b9e2-b00dc43720b5" /> Fixes #38187 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {createSortable} from '../modules/sortable.ts';
|
||||
import {DELETE, POST} from '../modules/fetch.ts';
|
||||
import {parseDom} from '../utils.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {performFetchAction} from './common-fetch-action.ts';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
@@ -101,8 +100,7 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
elMenu.querySelector(`.item[data-value="${CSS.escape(username)}"]`)?.classList.add('selected');
|
||||
};
|
||||
|
||||
type ProcessedResult = {value: string, name: string};
|
||||
const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items
|
||||
const processedResults: Record<string, string>[] = []; // to be used by dropdown to generate menu items
|
||||
const syncItemFromInput = () => {
|
||||
const inputVal = elSearchInput.value.trim();
|
||||
elItemFromInput.setAttribute('data-value', inputVal);
|
||||
@@ -115,10 +113,13 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
elSearchInput.value = selectedUsername;
|
||||
if (!searchUrl) {
|
||||
elSearchInput.addEventListener('input', syncItemFromInput);
|
||||
} else {
|
||||
if (!searchUrl.includes('?')) searchUrl += '?';
|
||||
$searchDropdown.dropdown('setting', 'apiSettings', {
|
||||
cache: false,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchUrl.includes('?')) searchUrl += '?';
|
||||
$searchDropdown.dropdown('setting', {
|
||||
onMenuUpdated: () => syncItemFromInput(),
|
||||
apiSettings: {
|
||||
url: `${searchUrl}&q={query}`,
|
||||
onResponse(resp: any) {
|
||||
// the content is provided by backend IssuePosters handler
|
||||
@@ -126,41 +127,16 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
for (const item of resp.results) {
|
||||
const htmlAvatar = html`<img class="ui avatar tw-align-middle" src="${item.avatar_link}" aria-hidden="true" alt width="20" height="20">`;
|
||||
const htmlFullName = item.full_name ? html`<span class="username-fullname">(${item.full_name})</span>` : '';
|
||||
const htmlItem = html`<span class="username-display">${htmlRaw(htmlAvatar)}<span>${item.username}</span>${htmlRaw(htmlFullName)}</span>`;
|
||||
const htmlItemInner = html`<span class="username-display">${htmlRaw(htmlAvatar)}<span>${item.username}</span>${htmlRaw(htmlFullName)}</span>`;
|
||||
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
||||
processedResults.push({value: item.username, name: htmlItem});
|
||||
const htmlItem = html`<div class="item" data-value="${item.username}">${htmlRaw(htmlItemInner)}</div>`;
|
||||
processedResults.push({type: 'html', html: htmlItem});
|
||||
}
|
||||
resp.results = processedResults;
|
||||
return resp;
|
||||
},
|
||||
});
|
||||
$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')};
|
||||
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
|
||||
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
|
||||
dropdownSetup.menu = function (values: any) {
|
||||
// remove old dynamic items
|
||||
for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
const newMenuHtml = dropdownTemplates.menu(values, $searchDropdown.dropdown('setting', 'fields'), true /* html */, $searchDropdown.dropdown('setting', 'className'));
|
||||
if (newMenuHtml) {
|
||||
const newMenuItems = parseDom(newMenuHtml, 'text/html').querySelectorAll('body > div');
|
||||
for (const newMenuItem of newMenuItems) {
|
||||
newMenuItem.classList.add('dynamic-item');
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('divider', 'dynamic-item');
|
||||
elMenu.append(div, ...newMenuItems);
|
||||
}
|
||||
$searchDropdown.dropdown('refresh');
|
||||
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
|
||||
setTimeout(() => syncItemFromInput(), 0);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initPinRemoveButton() {
|
||||
|
||||
Reference in New Issue
Block a user