refactor: replace Fomantic search module with first-party code (#37443)

- Replace fomantic `search` code with minimal first-party code
- Added a small fix to vertically align search box and search button
- Manually tested all search forms.
- Add `errorName` helper, similar to `errorMessage`.

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
silverwind
2026-05-11 07:25:26 +02:00
committed by GitHub
parent a603f89fce
commit 5dc9d621fd
15 changed files with 232 additions and 1663 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
import {GET, request} from '../modules/fetch.ts';
import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {addDelegatedEventListener, createElementFromHTML} from '../utils/dom.ts';
import {errorMessage} from '../modules/errors.ts';
import {errorMessage, errorName} from '../modules/errors.ts';
import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
@@ -138,7 +138,7 @@ async function performActionRequest(el: HTMLElement, opt: FetchActionOpts) {
}
await handleFetchActionError(resp);
} catch (err) {
if ((err as Error).name !== 'AbortError') {
if (errorName(err) !== 'AbortError') {
console.error(`Fetch action request error:`, err);
showErrorToast(`Error: ${errorMessage(err)}`);
}
+8 -23
View File
@@ -1,31 +1,16 @@
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import {htmlEscape} from '../../utils/html.ts';
import {attachSearchBox} from '../../modules/search.ts';
const {appSubUrl} = window.config;
type RepoSearchResponse = {data: Array<{repository: {full_name: string}}>};
export function initCompSearchRepoBox(el: HTMLElement) {
const uid = el.getAttribute('data-uid');
const exclusive = el.getAttribute('data-exclusive');
let url = `${appSubUrl}/repo/search?q={query}&uid=${uid}`;
if (exclusive === 'true') {
url += `&exclusive=true`;
}
fomanticQuery(el).search({
minCharacters: 2,
apiSettings: {
url,
onResponse(response: any) {
const items = [];
for (const item of response.data) {
items.push({
title: htmlEscape(item.repository.full_name.split('/')[1]),
description: htmlEscape(item.repository.full_name),
});
}
return {results: items};
},
},
searchFields: ['full_name'],
showNoResults: false,
});
if (exclusive === 'true') url += `&exclusive=true`;
attachSearchBox(el, url, (response: RepoSearchResponse) => response.data.map((item) => ({
title: item.repository.full_name.split('/')[1],
description: item.repository.full_name,
})));
}
+21 -40
View File
@@ -1,49 +1,30 @@
import {htmlEscape} from '../../utils/html.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import {attachSearchBox, type SearchResult} from '../../modules/search.ts';
const {appSubUrl} = window.config;
const looksLikeEmailAddressCheck = /^\S+@\S+$/;
type UserSearchResponse = {data: Array<{login: string; avatar_url: string; full_name: string}>};
export function initCompSearchUserBox() {
const searchUserBox = document.querySelector('#search-user-box');
if (!searchUserBox) return;
const box = document.querySelector<HTMLElement>('#search-user-box');
if (!box) return;
const allowEmailInput = searchUserBox.getAttribute('data-allow-email') === 'true';
const allowEmailDescription = searchUserBox.getAttribute('data-allow-email-description') ?? undefined;
const includeOrgs = searchUserBox.getAttribute('data-include-orgs') === 'true';
fomanticQuery(searchUserBox).search({
minCharacters: 2,
apiSettings: {
url: `${appSubUrl}/user/search_candidates?q={query}&orgs=${includeOrgs}`,
onResponse(response: any) {
const resultItems = [];
const searchQuery = searchUserBox.querySelector('input')!.value;
const searchQueryUppercase = searchQuery.toUpperCase();
for (const item of response.data) {
const resultItem = {
title: item.login,
image: item.avatar_url,
description: htmlEscape(item.full_name),
};
if (searchQueryUppercase === item.login.toUpperCase()) {
resultItems.unshift(resultItem); // add the exact match to the top
} else {
resultItems.push(resultItem);
}
}
const allowEmailInput = box.getAttribute('data-allow-email') === 'true';
const allowEmailDescription = box.getAttribute('data-allow-email-description') ?? undefined;
const includeOrgs = box.getAttribute('data-include-orgs') === 'true';
const url = `${appSubUrl}/user/search_candidates?q={query}&orgs=${includeOrgs}`;
if (allowEmailInput && !resultItems.length && looksLikeEmailAddressCheck.test(searchQuery)) {
const resultItem = {
title: searchQuery,
description: allowEmailDescription,
};
resultItems.push(resultItem);
}
return {results: resultItems};
},
},
searchFields: ['login', 'full_name'],
showNoResults: false,
attachSearchBox(box, url, (response: UserSearchResponse, query) => {
const items: SearchResult[] = [];
const queryUpper = query.toUpperCase();
for (const item of response.data) {
const result: SearchResult = {title: item.login, image: item.avatar_url, description: item.full_name};
if (queryUpper === item.login.toUpperCase()) items.unshift(result); // exact match floats to top
else items.push(result);
}
if (allowEmailInput && !items.length && looksLikeEmailAddressCheck.test(query)) {
items.push({title: query, description: allowEmailDescription});
}
return items;
});
}
+11 -22
View File
@@ -3,6 +3,7 @@ import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {attachSearchBox} from '../modules/search.ts';
import {globMatch} from '../utils/glob.ts';
const {appSubUrl} = window.config;
@@ -45,29 +46,17 @@ function initRepoSettingsCollaboration() {
}
}
function initRepoSettingsSearchTeamBox() {
const searchTeamBox = document.querySelector('#search-team-box');
if (!searchTeamBox) return;
type TeamSearchResponse = {data: Array<{name: string; permission: string}>};
fomanticQuery(searchTeamBox).search({
minCharacters: 2,
searchFields: ['name', 'description'],
showNoResults: false,
rawResponse: true,
apiSettings: {
url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`,
onResponse(response: any) {
const items: Array<Record<string, any>> = [];
for (const item of response.data) {
items.push({
title: item.name,
description: `${item.permission} access`, // TODO: translate this string
});
}
return {results: items};
},
},
});
function initRepoSettingsSearchTeamBox() {
const box = document.querySelector<HTMLElement>('#search-team-box');
if (!box) return;
const url = `${appSubUrl}/org/${box.getAttribute('data-org-name')}/teams/-/search?q={query}`;
attachSearchBox(box, url, (response: TeamSearchResponse) => response.data.map((item) => ({
title: item.name,
description: `${item.permission} access`, // TODO: translate this string
})));
}
function initRepoSettingsGitHook() {