From 207761ba1c7fc5135f708c34522f41ede9cc123c Mon Sep 17 00:00:00 2001 From: miwa Date: Sun, 10 May 2026 21:52:39 +0300 Subject: [PATCH] feat: implement user search in extension popup --- packages/core/src/github/client.ts | 5 ++ packages/core/src/github/rest-queries.ts | 16 +++++ packages/core/src/types.ts | 6 ++ packages/extension/entrypoints/background.ts | 11 +++ packages/extension/entrypoints/popup.html | 26 +++++++- packages/extension/entrypoints/popup/main.ts | 70 ++++++++++++++++++-- packages/extension/utils/messaging.ts | 18 ++++- 7 files changed, 144 insertions(+), 8 deletions(-) diff --git a/packages/core/src/github/client.ts b/packages/core/src/github/client.ts index 1087b45..1d9a7ad 100644 --- a/packages/core/src/github/client.ts +++ b/packages/core/src/github/client.ts @@ -64,6 +64,7 @@ import { countRepoPRsTotal, fetchVouchFile, postVouchComment, + searchUsers as searchUsersApi, } from "./rest-queries.js"; import { fetchScorecard } from "./scorecard.js"; @@ -522,5 +523,9 @@ export const createGitBazClient = (options: GitBazClientOptions = {}): GitBazCli getVouchStatus, isCollaborator, postVouchAction, + searchUsers: async (query: string) => { + const items = await searchUsersApi(octokit, query); + return items.map((i) => ({ login: i.login, avatarUrl: i.avatar_url })); + }, }; }; diff --git a/packages/core/src/github/rest-queries.ts b/packages/core/src/github/rest-queries.ts index 1040a92..89a2ff6 100644 --- a/packages/core/src/github/rest-queries.ts +++ b/packages/core/src/github/rest-queries.ts @@ -96,3 +96,19 @@ export const postVouchComment = async ( ); return response.data as { id: number; html_url: string }; }; + +/** Searches for users by login/username */ +export const searchUsers = async ( + octokit: Octokit, + query: string, +): Promise<{ login: string; avatar_url: string }[]> => { + try { + const response = await octokit.request("GET /search/users", { + q: query, + per_page: 5, + }); + return (response.data as { items: { login: string; avatar_url: string }[] }).items; + } catch { + return []; + } +}; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8d65cf2..adfe214 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -146,6 +146,11 @@ export interface UserStats { readonly automationReason?: string; } +export interface UserSearchResult { + readonly login: string; + readonly avatarUrl: string; +} + export interface ScoreComponent { readonly name: string; readonly rawValue: number; @@ -356,4 +361,5 @@ export interface GitBazClient { targetUsername: string, reason?: string, ): Promise; + searchUsers(query: string): Promise; } diff --git a/packages/extension/entrypoints/background.ts b/packages/extension/entrypoints/background.ts index ccf74fd..05939b6 100644 --- a/packages/extension/entrypoints/background.ts +++ b/packages/extension/entrypoints/background.ts @@ -152,6 +152,17 @@ export default defineBackground(() => { error: (error as Error).message, }); } + } else if (message.type === "SEARCH_USERS") { + try { + const client = await createClient(); + const result = await client.searchUsers(message.query); + sendResponse({ type: "SEARCH_USERS_RESULT", result }); + } catch (error) { + sendResponse({ + type: "SEARCH_USERS_RESULT", + error: (error as Error).message, + }); + } } else if (message.type === "GET_TOKEN") { const token = await getStoredToken(); sendResponse({ type: "TOKEN_RESULT", token }); diff --git a/packages/extension/entrypoints/popup.html b/packages/extension/entrypoints/popup.html index 6cc09a4..ae1956c 100644 --- a/packages/extension/entrypoints/popup.html +++ b/packages/extension/entrypoints/popup.html @@ -35,13 +35,37 @@ button.primary { background: #2da44e; color: #fff; border-color: #2da44e; } button.primary:hover { background: #218838; } #result { margin-top: 12px; white-space: pre-wrap; font-size: 12px; } + .search-results { + border: 1px solid #d0d7de; + border-radius: 6px; + margin-top: -4px; + margin-bottom: 8px; + background: #fff; + display: none; + max-height: 200px; + overflow-y: auto; + } + .search-result-item { + padding: 8px 10px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + } + .search-result-item:hover { background: #f6f8fa; } + .search-result-item img { + width: 20px; + height: 20px; + border-radius: 50%; + }

GitBaz

Checking...
- + +
diff --git a/packages/extension/entrypoints/popup/main.ts b/packages/extension/entrypoints/popup/main.ts index 367e0d4..d0c19f9 100644 --- a/packages/extension/entrypoints/popup/main.ts +++ b/packages/extension/entrypoints/popup/main.ts @@ -1,10 +1,14 @@ -import type { ScoreResponse, TokenResponse } from "../../utils/messaging.js"; +import type { ScoreResponse, SearchUsersResponse, TokenResponse } from "../../utils/messaging.js"; +import type { UserSearchResult } from "@happyhackingspace/gitbaz"; const authStatus = document.getElementById("auth-status") as HTMLDivElement; const usernameInput = document.getElementById("username") as HTMLInputElement; +const searchResults = document.getElementById("search-results") as HTMLDivElement; const lookupBtn = document.getElementById("lookup-btn") as HTMLButtonElement; const resultDiv = document.getElementById("result") as HTMLDivElement; +let searchTimeout: ReturnType; + const checkAuth = async (): Promise => { const response: TokenResponse = await chrome.runtime.sendMessage({ type: "GET_TOKEN" }); if (response.token) { @@ -15,12 +19,13 @@ const checkAuth = async (): Promise => { } }; -const lookupUser = async (): Promise => { - const username = usernameInput.value.trim(); +const lookupUser = async (usernameToUse?: string): Promise => { + const username = usernameToUse || usernameInput.value.trim(); if (!username) return; resultDiv.textContent = "Loading..."; lookupBtn.disabled = true; + searchResults.style.display = "none"; const response: ScoreResponse = await chrome.runtime.sendMessage({ type: "GET_SCORE", @@ -37,9 +42,64 @@ const lookupUser = async (): Promise => { lookupBtn.disabled = false; }; -lookupBtn.addEventListener("click", lookupUser); +const handleSearch = async (): Promise => { + const query = usernameInput.value.trim(); + if (query.length < 2) { + searchResults.style.display = "none"; + return; + } + + const response: SearchUsersResponse = await chrome.runtime.sendMessage({ + type: "SEARCH_USERS", + query, + }); + + if (response.result && response.result.length > 0) { + displayResults(response.result); + } else { + searchResults.style.display = "none"; + } +}; + +const displayResults = (users: readonly UserSearchResult[]): void => { + searchResults.innerHTML = ""; + searchResults.style.display = "block"; + + for (const user of users) { + const item = document.createElement("div"); + item.className = "search-result-item"; + item.innerHTML = ` + ${user.login} + ${user.login} + `; + item.addEventListener("click", () => { + usernameInput.value = user.login; + searchResults.style.display = "none"; + lookupUser(user.login); + }); + searchResults.appendChild(item); + } +}; + +lookupBtn.addEventListener("click", () => lookupUser()); + +usernameInput.addEventListener("input", () => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(handleSearch, 300); +}); + usernameInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") lookupUser(); + if (e.key === "Enter") { + clearTimeout(searchTimeout); + lookupUser(); + } +}); + +// Hide search results when clicking outside +document.addEventListener("click", (e) => { + if (!usernameInput.contains(e.target as Node) && !searchResults.contains(e.target as Node)) { + searchResults.style.display = "none"; + } }); checkAuth(); diff --git a/packages/extension/utils/messaging.ts b/packages/extension/utils/messaging.ts index 683132e..e9a3ac9 100644 --- a/packages/extension/utils/messaging.ts +++ b/packages/extension/utils/messaging.ts @@ -9,6 +9,7 @@ import type { RepoContext, RepositoryContext, ScoreResult, + UserSearchResult, VouchAction, VouchActionResult, VouchLookupResult, @@ -157,6 +158,17 @@ export interface OpenSettingsRequest { type: "OPEN_SETTINGS"; } +export interface SearchUsersRequest { + type: "SEARCH_USERS"; + query: string; +} + +export interface SearchUsersResponse { + type: "SEARCH_USERS_RESULT"; + result?: readonly UserSearchResult[]; + error?: string; +} + export type ExtensionMessage = | ScoreRequest | PullRequestRequest @@ -170,7 +182,8 @@ export type ExtensionMessage = | VouchActionRequest | BusFactorRequest | BlameAnalysisRequest - | OpenSettingsRequest; + | OpenSettingsRequest + | SearchUsersRequest; export type ExtensionResponse = | ScoreResponse @@ -184,4 +197,5 @@ export type ExtensionResponse = | CollaboratorResponse | VouchActionResponse | BusFactorResponse - | BlameAnalysisResponse; + | BlameAnalysisResponse + | SearchUsersResponse;