feat: scope mobile search by type and status (Closes #793)
CI / lint (pull_request) Successful in 1m45s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 02:34:44 +00:00
1 parent 3c65bc1b31
commit bd054998b2
14 files changed
+315 -37

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ FEATURE_SOURCES = {
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
"security-center": ("static/security-center.js",),
"today-timer": (
"static/task-overlay-history.js", "static/search-preview.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/commands.js", "static/task-overlay-history.js", "static/search-preview.js", "static/my-work.js", "static/protect-today.js", "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/update-triage-session.js", "static/update-review-handoff.js", "static/update-triage-launcher.js", "static/update-triage-gesture.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
+21 -9
View File
@@ -463,14 +463,27 @@ def _normalize_global_search_item(item: Any, kind: str) -> dict | None:
}
async def global_search(query: str, limit: int = 10, page: int = 1) -> dict:
async def global_search(
query: str,
limit: int = 10,
page: int = 1,
kind: str = "all",
state: str = "all",
) -> dict:
"""Search accessible issues and pulls concurrently with balanced pagination."""
stream_limit = (limit + 1) // 2
item_types = ("issues", "pulls") if kind == "all" else (
"issues" if kind == "issue" else "pulls",
)
stream_limit = (limit + 1) // 2 if kind == "all" else limit
async def load(item_type: str) -> Any:
params = {
"q": query, "type": item_type, "state": state,
"limit": stream_limit, "page": page,
}
response = await _get_client().get(
"/api/v1/repos/issues/search",
headers=_auth(),
params={"q": query, "type": item_type, "limit": stream_limit, "page": page},
params=params,
)
response.raise_for_status()
payload = response.json()
@@ -478,9 +491,7 @@ async def global_search(query: str, limit: int = 10, page: int = 1) -> dict:
raise ValueError("Gitea global search response was not a list")
return payload
outcomes = await asyncio.gather(
load("issues"), load("pulls"), return_exceptions=True
)
outcomes = await asyncio.gather(*(load(item_type) for item_type in item_types), return_exceptions=True)
for outcome in outcomes:
if isinstance(outcome, asyncio.CancelledError):
raise outcome
@@ -489,15 +500,16 @@ async def global_search(query: str, limit: int = 10, page: int = 1) -> dict:
streams: list[list[dict]] = []
seen: set[tuple[str, str, int]] = set()
for outcome, kind in zip(outcomes, ("issue", "pull"), strict=True):
result_kinds = tuple("issue" if item_type == "issues" else "pull" for item_type in item_types)
for outcome, result_kind in zip(outcomes, result_kinds, strict=True):
normalized_stream = []
if isinstance(outcome, BaseException):
streams.append(normalized_stream)
continue
for item in outcome:
normalized = _normalize_global_search_item(item, kind)
normalized = _normalize_global_search_item(item, result_kind)
if normalized is not None:
identity = (kind, normalized["repository"], normalized["number"])
identity = (result_kind, normalized["repository"], normalized["number"])
if identity in seen:
continue
seen.add(identity)
+17 -6
View File
@@ -1006,11 +1006,15 @@ def _share_target_login_redirect(request: Request) -> str:
search_values = request.query_params.getlist("search")
preview_values = request.query_params.getlist("preview")
if search_values or preview_values:
allowed = {"search", "preview"}
allowed = {"search", "preview", "search_kind", "search_state"}
kind_values = request.query_params.getlist("search_kind")
state_values = request.query_params.getlist("search_state")
if (
set(request.query_params.keys()) - allowed
or len(search_values) != 1
or len(preview_values) != 1
or len(kind_values) > 1
or len(state_values) > 1
or len(search_values[0]) > 200
or len(preview_values[0]) > 200
or not re.fullmatch(
@@ -1019,9 +1023,14 @@ def _share_target_login_redirect(request: Request) -> str:
)
):
return "login"
continuation = urlencode(
(("search", search_values[0].strip()), ("preview", preview_values[0]))
)
continuation_values = [
("search", search_values[0].strip()), ("preview", preview_values[0]),
]
if kind_values and kind_values[0] in {"all", "issue", "pull"}:
continuation_values.append(("search_kind", kind_values[0]))
if state_values and state_values[0] in {"all", "open", "closed"}:
continuation_values.append(("search_state", state_values[0]))
continuation = urlencode(continuation_values)
return f"login?{urlencode({'continue': f'./?{continuation}'})}"
limits = {"title": 200, "text": 8000, "url": 2048}
if any(
@@ -2712,13 +2721,15 @@ async def global_search(
q: str = Query(min_length=2, max_length=100),
limit: int = Query(default=10, ge=1, le=25),
page: int = Query(default=1, ge=1, le=100),
kind: Literal["all", "issue", "pull"] = Query(default="all"),
state: Literal["all", "open", "closed"] = Query(default="all"),
) -> JSONResponse:
query = q.strip()
if len(query) < 2:
raise HTTPException(status_code=422, detail="Search query must contain at least 2 characters")
try:
result = await asyncio.wait_for(
gitea_proxy.global_search(query, limit, page),
gitea_proxy.global_search(query, limit, page, kind, state),
timeout=GLOBAL_SEARCH_TIMEOUT_SECONDS,
)
except Exception:
@@ -2727,7 +2738,7 @@ async def global_search(
status_code=503,
headers={"Retry-After": "1"},
)
return JSONResponse({"query": query, **result})
return JSONResponse({"query": query, "scope": {"kind": kind, "state": state}, **result})
@app.get("/api/v1/repos/{owner}/{repo}/issues/{number}/preview")