feat(mobile): show live PR status on inbox report cards and detail (port #2695, #2694)#2698
Conversation
…ort #2695, #2694) Inbox signal reports that have an `implementation_pr_url` now render a live PR-status badge (open / merged / closed / draft) on the report list rows, the tinder card, and the report detail screen — reusing the existing `usePrStatus` hook and `PrStatusBadge` component from the tasks feature. The badge only renders for a canonical GitHub PR URL and renders nothing while the state is loading or unresolvable (private repo / 404 / unparseable), rather than showing a misleading default "open" badge. This is gated by a new `hideWhenUnresolved` prop on `PrStatusBadge`; the existing task-header usage keeps its always-tappable neutral badge. A `size="sm"` variant keeps the badge compact on dense list rows. Ports the desktop behavior from #2695 (list cards) and #2694 (detail) to the mobile app. The desktop GraphQL batching is intentionally not ported; mobile relies on react-query caching of the public GitHub REST API. Generated-By: PostHog Code Task-Id: ba8e678f-1c24-4c21-abe3-5dc204adec1a
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx:55-87
**Non-parameterised state rendering tests**
The four individual state cases (open, merged, closed, draft) repeat the same structure — call `setStatus`, render with an identical `prUrl`, assert on `label()`. Per the team's convention we always prefer parameterised tests; these are a natural fit for `it.each`. The merged case has one extra icon assertion that can be captured as an optional field in the test-case object (e.g. `expectedIcon?: string` defaulting to `"GitPullRequest"`), keeping the whole suite in one table.
### Issue 2 of 2
apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx:55-111
**New `size` prop is untested**
The PR adds a `size` prop with distinct `box` class and `iconSize` values for `"sm"` vs `"md"`, but no test asserts on either. The `iconSize` logic (`16` vs `20`) is passed as a prop to the icon, so it is observable in the rendered tree. Without a test, a future refactor could silently regress the compact badge on list rows.
Reviews (1): Last reviewed commit: "feat(mobile): show live PR status on inb..." | Re-trigger Greptile |
| state: "open", | ||
| merged: false, | ||
| draft: false, | ||
| additions: 0, | ||
| deletions: 0, | ||
| }; | ||
|
|
||
| describe("PrStatusBadge", () => { | ||
| it("renders an open PR badge", () => { | ||
| setStatus({ ...base, state: "open" }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open PR"); | ||
| expect(iconCount(r, "GitPullRequest")).toBe(1); | ||
| }); | ||
|
|
||
| it("renders a merged PR badge with the merge icon", () => { | ||
| setStatus({ ...base, state: "closed", merged: true }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open merged PR"); | ||
| expect(iconCount(r, "GitMerge")).toBe(1); | ||
| expect(iconCount(r, "GitPullRequest")).toBe(0); | ||
| }); | ||
|
|
||
| it("renders a closed PR badge", () => { | ||
| setStatus({ ...base, state: "closed" }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open closed PR"); | ||
| }); | ||
|
|
||
| it("renders a draft PR badge", () => { | ||
| setStatus({ ...base, state: "open", draft: true }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open draft PR"); |
There was a problem hiding this comment.
Non-parameterised state rendering tests
The four individual state cases (open, merged, closed, draft) repeat the same structure — call setStatus, render with an identical prUrl, assert on label(). Per the team's convention we always prefer parameterised tests; these are a natural fit for it.each. The merged case has one extra icon assertion that can be captured as an optional field in the test-case object (e.g. expectedIcon?: string defaulting to "GitPullRequest"), keeping the whole suite in one table.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx
Line: 55-87
Comment:
**Non-parameterised state rendering tests**
The four individual state cases (open, merged, closed, draft) repeat the same structure — call `setStatus`, render with an identical `prUrl`, assert on `label()`. Per the team's convention we always prefer parameterised tests; these are a natural fit for `it.each`. The merged case has one extra icon assertion that can be captured as an optional field in the test-case object (e.g. `expectedIcon?: string` defaulting to `"GitPullRequest"`), keeping the whole suite in one table.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| state: "open", | ||
| merged: false, | ||
| draft: false, | ||
| additions: 0, | ||
| deletions: 0, | ||
| }; | ||
|
|
||
| describe("PrStatusBadge", () => { | ||
| it("renders an open PR badge", () => { | ||
| setStatus({ ...base, state: "open" }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open PR"); | ||
| expect(iconCount(r, "GitPullRequest")).toBe(1); | ||
| }); | ||
|
|
||
| it("renders a merged PR badge with the merge icon", () => { | ||
| setStatus({ ...base, state: "closed", merged: true }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open merged PR"); | ||
| expect(iconCount(r, "GitMerge")).toBe(1); | ||
| expect(iconCount(r, "GitPullRequest")).toBe(0); | ||
| }); | ||
|
|
||
| it("renders a closed PR badge", () => { | ||
| setStatus({ ...base, state: "closed" }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open closed PR"); | ||
| }); | ||
|
|
||
| it("renders a draft PR badge", () => { | ||
| setStatus({ ...base, state: "open", draft: true }); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(label(r)).toBe("Open draft PR"); | ||
| }); | ||
|
|
||
| it.each([ | ||
| { data: undefined, label: "loading" }, | ||
| { data: null, label: "unresolved (private/404/non-GitHub)" }, | ||
| ])( | ||
| "renders nothing when hideWhenUnresolved is set and status is $label", | ||
| ({ data }) => { | ||
| setStatus(data); | ||
| const r = render({ | ||
| prUrl: "https://github.com/a/b/pull/1", | ||
| hideWhenUnresolved: true, | ||
| }); | ||
| expect(r.toJSON()).toBeNull(); | ||
| }, | ||
| ); | ||
|
|
||
| it("still renders a neutral badge for an unresolved PR by default", () => { | ||
| setStatus(null); | ||
| const r = render({ prUrl: "https://github.com/a/b/pull/1" }); | ||
| expect(r.toJSON()).not.toBeNull(); | ||
| expect(label(r)).toBe("Open PR"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The PR adds a size prop with distinct box class and iconSize values for "sm" vs "md", but no test asserts on either. The iconSize logic (16 vs 20) is passed as a prop to the icon, so it is observable in the rendered tree. Without a test, a future refactor could silently regress the compact badge on list rows.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/src/features/tasks/components/PrStatusBadge.test.tsx
Line: 55-111
Comment:
**New `size` prop is untested**
The PR adds a `size` prop with distinct `box` class and `iconSize` values for `"sm"` vs `"md"`, but no test asserts on either. The `iconSize` logic (`16` vs `20`) is passed as a prop to the icon, so it is observable in the rendered tree. Without a test, a future refactor could silently regress the compact badge on list rows.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Low-risk additive UI change in the mobile app layer — new optional props with safe defaults, no business logic, no data model or API contract changes. The two bot comments are P2 style/coverage notes (non-parameterised tests and untested size prop); neither represents a production risk or violates a hard rule.
Summary
Ports two desktop PRs to the React Native / Expo mobile app:
Inbox signal reports that carry an
implementation_pr_urlnow render a small live PR-status badge (open / merged / closed / draft) on:ReportListRow)SwipeableReportCard)app/inbox/[...id].tsx)It reuses the existing mobile pieces rather than reinventing them:
usePrStatus— fetches live PR state from GitHub's public REST API, returningnullfor private/unresolvable PRs.PrStatusBadge— the color-coded open/merged/closed/draft badge.Behavior (matches desktop)
This is driven by a new
hideWhenUnresolvedprop onPrStatusBadge. The existing task-header usage is unchanged — it keeps its always-tappable neutral badge. Asize="sm"variant keeps the badge compact on dense list rows.The desktop GraphQL batching of PR state is intentionally not ported; mobile relies on react-query caching over the public GitHub REST API.
Tests
Added
PrStatusBadge.test.tsxcovering: open / merged / closed / draft rendering, and rendering nothing whenhideWhenUnresolvedis set and the status is loading or unresolved.Scope
No desktop code changed. Changes are confined to
apps/mobile.