feat: open requested reviews from mobile search (Closes #791)
All checks were successful
CI / lint (pull_request) Successful in 1m45s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-14 01:44:51 +00:00
parent 9ae0a89970
commit bc082f2bd9
4 changed files with 74 additions and 5 deletions

View File

@ -4364,9 +4364,9 @@
claimButton.hidden = !(detail.claimable || detail.assigned_to_me);
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
claimButton.disabled = state.status === 'claiming';
startButton.hidden = !(detail.kind === 'issue' && (detail.reopenable ||
(detail.state === 'open' && (detail.claimable || detail.assigned_to_me))));
startButton.textContent = detail.reopenable ? 'Reopen & resume' :
startButton.hidden = !(detail.reviewable || (detail.kind === 'issue' &&
(detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me)))));
startButton.textContent = detail.reviewable ? 'Review now' : detail.reopenable ? 'Reopen & resume' :
(detail.assigned_to_me ? 'Start in Today' : 'Assign & start');
startButton.disabled = state.status === 'claiming' || state.status === 'reopening';
const shareStatus = {
@ -4380,7 +4380,7 @@
else if (detail.claimable) status.textContent = 'This issue is open and unassigned.';
else if (detail.assigned_to_me) status.textContent = 'This item is already in My Work.';
else if (detail.reopenable) status.textContent = 'Closed—reopen to resume.';
else status.textContent = 'Read-only preview.';
else status.textContent = 'Ready.';
}
const searchPreview = createSearchPreview({
fetchJson: item => fetchReviewJson(searchPreviewPath(item), { headers:{ Accept:'application/json' } }),
@ -4657,6 +4657,11 @@
});
qs('#start-search-result').addEventListener('click', async () => {
const detail = searchPreviewDetail;
if (detail?.reviewable) {
taskOverlayHistory.leave();
detail.kind = 'review';
return openRoutedWork(detail, null, { replace:true });
}
if (!detail || detail.kind !== 'issue') return;
try {
if (detail.reopenable) await searchReopenAndStart.run(detail);

View File

@ -539,6 +539,19 @@ async def work_preview(repository: str, kind: str, number: int) -> dict:
login = user.get("login") if isinstance(user, dict) else ""
state = issue.get("state") if isinstance(issue.get("state"), str) else ""
actual_kind = "pull" if isinstance(issue.get("pull_request"), dict) else "issue"
reviewable = False
if actual_kind == "pull" and state == "open" and login:
try:
pull = await fetch(f"repos/{repository}/pulls/{number}")
except Exception:
pull = None
requested_reviewers = pull.get("requested_reviewers") if isinstance(pull, dict) else []
if not isinstance(requested_reviewers, list):
requested_reviewers = []
reviewable = any(
isinstance(reviewer, dict) and reviewer.get("login") == login
for reviewer in requested_reviewers
)
return {
"kind": actual_kind,
"repository": repository,
@ -556,6 +569,7 @@ async def work_preview(repository: str, kind: str, number: int) -> dict:
"claimable": actual_kind == "issue" and state == "open" and not assignee_names,
"reopenable": actual_kind == "issue" and state == "closed",
"assigned_to_me": bool(login and login in assignee_names),
"reviewable": reviewable,
}

View File

@ -398,6 +398,23 @@ def test_assigned_pull_preview_hands_off_to_existing_my_work_sheet_without_claim
assert "await openPreviewWorkInMyWork(claimed)" in click_handler
def test_requested_pull_preview_opens_native_review_without_assignment_mutation():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
assert "startButton.hidden = !(detail.reviewable" in html
assert "detail.reviewable ? 'Review now'" in html
handler = html.split(
"qs('#start-search-result').addEventListener('click'", 1
)[1].split("qs('#close-whiteboard')", 1)[0]
assert "detail.kind = 'review'" in handler
assert "openRoutedWork" in handler
assert "{ replace:true }" in handler
assert "searchPreview.claim" not in handler
assert "fetchReviewJson" not in handler
assert ".search-preview-actions button, .search-preview-actions a { min-height:44px;" in css
def test_search_preview_offers_assign_and_start_for_eligible_issues():
html = dashboard_bundle_text()
css = (FRONTEND / "dashboard.css").read_text()
@ -413,7 +430,7 @@ def test_search_preview_offers_assign_and_start_for_eligible_issues():
def test_closed_issue_preview_reopens_then_resumes_through_capacity_guard():
html = dashboard_bundle_text()
assert "startButton.textContent = detail.reopenable ? 'Reopen & resume'" in html
assert "detail.reopenable ? 'Reopen & resume'" in html
assert "mutate: (detail, action) => fetchReviewJson(" in html
handler = html.split("qs('#start-search-result').addEventListener('click'", 1)[1].split(
"qs('#close-whiteboard')", 1

View File

@ -267,6 +267,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i
"claimable": True,
"reopenable": False,
"assigned_to_me": False,
"reviewable": False,
}
@ -294,6 +295,38 @@ async def test_work_preview_derives_pull_kind_and_never_offers_issue_claim():
assert preview["claimable"] is False
@pytest.mark.anyio
async def test_work_preview_marks_authoritative_requested_pull_review_actionable():
requested_paths = []
async def handler(request):
requested_paths.append(request.url.path)
if request.url.path.endswith("/user"):
return httpx.Response(200, json={"login": "timmy"})
if request.url.path.endswith("/pulls/9"):
return httpx.Response(200, json={
"state": "open",
"requested_reviewers": [{"login": "timmy"}, None],
})
return httpx.Response(200, json={
"number": 9,
"title": "Improve search",
"state": "open",
"html_url": "https://forge.example/stackchain/web/pulls/9",
"pull_request": {"merged": False},
"assignees": [],
})
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
try:
preview = await gitea_proxy.work_preview("stackchain/web", "issue", 9)
finally:
await gitea_proxy.stop_client()
assert preview["reviewable"] is True
assert "/api/v1/repos/stackchain/web/pulls/9" in requested_paths
@pytest.mark.anyio
async def test_work_preview_offers_reopen_only_for_closed_issues():
async def handler(request):