From 690797b88f69c343fb12dda48e8a05171d191b00 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 14:47:39 -0700 Subject: [PATCH 1/6] Record PR assignees and surface them on the frontend Capture the GitHub `assignees[]` array for pull requests, persist it in the `pulls` table (JSON-encoded in a new text column), emit it to the frontend, and show a small person icon on a pull card when the current user is one of the assignees. Mirrors the existing Issue-assignee handling (backend/DB) and the `participants` array (frontend shape + "includes me" conditional UI). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/pull-card/assignee.tsx | 23 +++++++++++++++++++++++ frontend/src/pull-card/index.tsx | 2 ++ frontend/src/pull.ts | 4 ++++ frontend/src/theme/day_theme.less | 2 ++ frontend/src/theme/night_theme.less | 2 ++ frontend/src/types.ts | 1 + migrations/0020-pulls--add-assignees.sql | 1 + migrations/schema.sql | 1 + models/db_pull.js | 1 + models/pull.js | 2 ++ 10 files changed, 39 insertions(+) create mode 100644 frontend/src/pull-card/assignee.tsx create mode 100644 migrations/0020-pulls--add-assignees.sql diff --git a/frontend/src/pull-card/assignee.tsx b/frontend/src/pull-card/assignee.tsx new file mode 100644 index 00000000..b58d75dc --- /dev/null +++ b/frontend/src/pull-card/assignee.tsx @@ -0,0 +1,23 @@ +import { Pull } from "../pull"; +import { + Box +} from "@chakra-ui/react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faUserCheck, +} from "@fortawesome/free-solid-svg-icons"; + +export function Assignee({ pull }: { pull: Pull }) { + if (!pull.assignedToMe()) { + return null; + } + return ( + + + + ); +} diff --git a/frontend/src/pull-card/index.tsx b/frontend/src/pull-card/index.tsx index cf8aa21d..af87138f 100644 --- a/frontend/src/pull-card/index.tsx +++ b/frontend/src/pull-card/index.tsx @@ -1,6 +1,7 @@ import { Pull } from "../pull"; import { CommitStatuses } from "./commit-statuses"; import { Participants } from "./participants"; +import { Assignee } from "./assignee"; import { Age } from "./age"; import { Flags } from "./flags"; import { Avatar } from "./avatar"; @@ -69,6 +70,7 @@ export const PullCard = memo(function PullCard({ return ( + diff --git a/frontend/src/pull.ts b/frontend/src/pull.ts index 38236b2f..8e7863c8 100644 --- a/frontend/src/pull.ts +++ b/frontend/src/pull.ts @@ -84,6 +84,10 @@ export class Pull extends PullData { return this.participants && this.participants.includes(getUser()); } + assignedToMe(): boolean { + return this.assignees?.includes(getUser()) ?? false; + } + hasMyDevBlock(): boolean { return this.getDevBlock()?.data.user.login == getUser(); } diff --git a/frontend/src/theme/day_theme.less b/frontend/src/theme/day_theme.less index cd9887be..7926a90f 100644 --- a/frontend/src/theme/day_theme.less +++ b/frontend/src/theme/day_theme.less @@ -178,6 +178,8 @@ body[data-theme="day_theme"] { --participants-without-me: @grey; --participants-including-me: @blue; + --assignee-icon: darken(@green, 10%); + // Lines Changed --additions: darken(@green, 10%); --deletions: @red; diff --git a/frontend/src/theme/night_theme.less b/frontend/src/theme/night_theme.less index 4f29943c..de7a2e1c 100644 --- a/frontend/src/theme/night_theme.less +++ b/frontend/src/theme/night_theme.less @@ -171,6 +171,8 @@ body[data-theme="night_theme"] { --participants-without-me: darken(@grey, 15%); --participants-including-me: @blue; + --assignee-icon: @green; + --copy-branch: darken(@grey, 15%); --copy-branch-hover: @blue; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 75d605c3..2f046620 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -125,4 +125,5 @@ export class PullData { }; labels: Label[]; participants: string[]; + assignees: string[]; } diff --git a/migrations/0020-pulls--add-assignees.sql b/migrations/0020-pulls--add-assignees.sql new file mode 100644 index 00000000..f9205878 --- /dev/null +++ b/migrations/0020-pulls--add-assignees.sql @@ -0,0 +1 @@ +ALTER TABLE `pulls` ADD COLUMN `assignees` text COLLATE utf8mb4_general_ci DEFAULT NULL AFTER `owner`; diff --git a/migrations/schema.sql b/migrations/schema.sql index b4aa0753..42fa0c9f 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -140,6 +140,7 @@ CREATE TABLE IF NOT EXISTS `pulls` ( `head_sha` char(40) COLLATE utf8mb4_general_ci NOT NULL, `base_branch` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, `owner` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, + `assignees` text COLLATE utf8mb4_general_ci DEFAULT NULL, `cr_req` int NOT NULL DEFAULT '2', `qa_req` int NOT NULL DEFAULT '1', `date` int unsigned DEFAULT NULL, diff --git a/models/db_pull.js b/models/db_pull.js index f762fd9a..9c6cfc84 100644 --- a/models/db_pull.js +++ b/models/db_pull.js @@ -25,6 +25,7 @@ function DBPull(pull) { head_sha: pullData.head.sha, base_branch: pullData.base.ref, owner: getLogin(pullData.user), + assignees: JSON.stringify(pullData.assignees || []), cr_req: pullData.cr_req, qa_req: pullData.qa_req, closes: pullData.closes, diff --git a/models/pull.js b/models/pull.js index 60ed4c2a..75df9b72 100644 --- a/models/pull.js +++ b/models/pull.js @@ -225,6 +225,7 @@ class Pull { user: { login: getLogin(data.user), }, + assignees: (data.assignees || []).map((a) => getLogin(a)), commits: data.commits, additions: data.additions, deletions: data.deletions, @@ -280,6 +281,7 @@ class Pull { user: { login: data.owner, }, + assignees: data.assignees ? JSON.parse(data.assignees) : [], cr_req: data.cr_req, qa_req: data.qa_req, }; From ada57beab3173ce875b087d66d7dd4afcc92791d Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 15:13:40 -0700 Subject: [PATCH 2/6] Render assignee icon inline and add dummy-pull assignees Move the "assigned to you" icon from the top-left corner (where it was hidden behind the commit-status bar) to inline before the pull title. Add assignees to a few dummy pulls to exercise both UI states. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/dummy-pulls.json | 4 ++++ frontend/src/pull-card/assignee.tsx | 10 +++++----- frontend/src/pull-card/index.tsx | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/dummy-pulls.json b/frontend/dummy-pulls.json index 3511efd0..68059c66 100644 --- a/frontend/dummy-pulls.json +++ b/frontend/dummy-pulls.json @@ -29,6 +29,7 @@ "user": { "login": "BaseInfinity" }, + "assignees": ["danielbeardsley"], "cr_req": 2, "qa_req": 1, "status": { @@ -171,6 +172,7 @@ "user": { "login": "ianrohde" }, + "assignees": ["danielbeardsley", "ianrohde"], "cr_req": 2, "qa_req": 1, "status": { @@ -481,6 +483,7 @@ "user": { "login": "hackalot805" }, + "assignees": ["ardelato"], "cr_req": 2, "qa_req": 1, "status": { @@ -697,6 +700,7 @@ "user": { "login": "rjmccluskey" }, + "assignees": ["hackalot805", "ardelato"], "cr_req": 2, "qa_req": 1, "status": { diff --git a/frontend/src/pull-card/assignee.tsx b/frontend/src/pull-card/assignee.tsx index b58d75dc..a98bb10d 100644 --- a/frontend/src/pull-card/assignee.tsx +++ b/frontend/src/pull-card/assignee.tsx @@ -1,23 +1,23 @@ import { Pull } from "../pull"; -import { - Box -} from "@chakra-ui/react"; +import { chakra } from "@chakra-ui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUserCheck, } from "@fortawesome/free-solid-svg-icons"; +// Rendered inline before the pull title; only shown when the current user +// is one of the pull's assignees. export function Assignee({ pull }: { pull: Pull }) { if (!pull.assignedToMe()) { return null; } return ( - + - + ); } diff --git a/frontend/src/pull-card/index.tsx b/frontend/src/pull-card/index.tsx index af87138f..e3f2ae9d 100644 --- a/frontend/src/pull-card/index.tsx +++ b/frontend/src/pull-card/index.tsx @@ -70,7 +70,6 @@ export const PullCard = memo(function PullCard({ return ( - @@ -90,6 +89,7 @@ export const PullCard = memo(function PullCard({ ) : ( )} + {pull.getRepoName()} #{pull.number}:{" "} @@ -181,6 +181,7 @@ export const ClosedPullCard = memo(function ClosedPullCard({ ) : ( )} + {pull.getRepoName()} #{pull.number}:{" "} From 601a107c0463b0e64c99c0d1c55cfb18daa318e8 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 15:19:52 -0700 Subject: [PATCH 3/6] Fold assignee state into the participants indicator Replace the separate assignee component with color on the existing participants icon: green when assigned to you, blue when participating, grey otherwise. Icon shape still encodes participant quantity, and the tooltip reflects the assigned state. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/pull-card/assignee.tsx | 23 ------------ frontend/src/pull-card/index.tsx | 3 -- frontend/src/pull-card/participants.tsx | 47 ++++++++++++++++++------- 3 files changed, 34 insertions(+), 39 deletions(-) delete mode 100644 frontend/src/pull-card/assignee.tsx diff --git a/frontend/src/pull-card/assignee.tsx b/frontend/src/pull-card/assignee.tsx deleted file mode 100644 index a98bb10d..00000000 --- a/frontend/src/pull-card/assignee.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Pull } from "../pull"; -import { chakra } from "@chakra-ui/react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faUserCheck, -} from "@fortawesome/free-solid-svg-icons"; - -// Rendered inline before the pull title; only shown when the current user -// is one of the pull's assignees. -export function Assignee({ pull }: { pull: Pull }) { - if (!pull.assignedToMe()) { - return null; - } - return ( - - - - ); -} diff --git a/frontend/src/pull-card/index.tsx b/frontend/src/pull-card/index.tsx index e3f2ae9d..cf8aa21d 100644 --- a/frontend/src/pull-card/index.tsx +++ b/frontend/src/pull-card/index.tsx @@ -1,7 +1,6 @@ import { Pull } from "../pull"; import { CommitStatuses } from "./commit-statuses"; import { Participants } from "./participants"; -import { Assignee } from "./assignee"; import { Age } from "./age"; import { Flags } from "./flags"; import { Avatar } from "./avatar"; @@ -89,7 +88,6 @@ export const PullCard = memo(function PullCard({ ) : ( )} - {pull.getRepoName()} #{pull.number}:{" "} @@ -181,7 +179,6 @@ export const ClosedPullCard = memo(function ClosedPullCard({ ) : ( )} - {pull.getRepoName()} #{pull.number}:{" "} diff --git a/frontend/src/pull-card/participants.tsx b/frontend/src/pull-card/participants.tsx index d999ee25..c6b2ca42 100644 --- a/frontend/src/pull-card/participants.tsx +++ b/frontend/src/pull-card/participants.tsx @@ -8,30 +8,51 @@ import { faUsers, } from "@fortawesome/free-solid-svg-icons"; +// A single indicator whose shape encodes the number of participants +// (one / many / none-is-blank) and whose color encodes the current user's +// relationship to the pull: assigned, participating, or neither. export function Participants({ pull }: { pull: Pull }) { - if (!pull.participants?.length) { + const count = pull.participants?.length ?? 0; + if (!count && !pull.assignedToMe()) { return null; } return ( 1 ? faUsers : faUser} + icon={count > 1 ? faUsers : faUser} title={tooltip(pull)} - color={pull.participating() ? "var(--participants-including-me)" : "var(--participants-without-me)"} + color={color(pull)} /> ); } -function tooltip(pull: Pull) { - if (pull.participants.length == 1) { - return pull.participating() ? - "Only you participating" : - "1 participant" - } else { - return pull.participating() ? - `${pull.participants.length} participants (including you)` : - `${pull.participants.length} participants`; - } +function color(pull: Pull) { + if (pull.assignedToMe()) { + return "var(--assignee-icon)"; + } + return pull.participating() + ? "var(--participants-including-me)" + : "var(--participants-without-me)"; } +function tooltip(pull: Pull) { + const count = pull.participants?.length ?? 0; + const participants = + count === 0 + ? null + : count === 1 + ? pull.participating() + ? "only you participating" + : "1 participant" + : pull.participating() + ? `${count} participants (including you)` + : `${count} participants`; + + if (pull.assignedToMe()) { + return participants ? `Assigned to you, ${participants}` : "Assigned to you"; + } + return participants + ? participants.charAt(0).toUpperCase() + participants.slice(1) + : ""; +} From 9f27e7b89afd9a0094a34e6a0cc36af9f4f75d79 Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 15:27:08 -0700 Subject: [PATCH 4/6] Store pull assignees in a JSON column Change the `assignees` column from text to a native JSON column. mysql2 auto-parses JSON columns on read, so getFromDB no longer JSON.parses the value; the write path keeps JSON.stringify to safely populate the column. Co-Authored-By: Claude Opus 4.8 (1M context) --- migrations/0020-pulls--add-assignees.sql | 2 +- migrations/schema.sql | 2 +- models/db_pull.js | 1 + models/pull.js | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/migrations/0020-pulls--add-assignees.sql b/migrations/0020-pulls--add-assignees.sql index f9205878..4cb3b190 100644 --- a/migrations/0020-pulls--add-assignees.sql +++ b/migrations/0020-pulls--add-assignees.sql @@ -1 +1 @@ -ALTER TABLE `pulls` ADD COLUMN `assignees` text COLLATE utf8mb4_general_ci DEFAULT NULL AFTER `owner`; +ALTER TABLE `pulls` ADD COLUMN `assignees` json DEFAULT NULL AFTER `owner`; diff --git a/migrations/schema.sql b/migrations/schema.sql index 42fa0c9f..da2919b9 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -140,7 +140,7 @@ CREATE TABLE IF NOT EXISTS `pulls` ( `head_sha` char(40) COLLATE utf8mb4_general_ci NOT NULL, `base_branch` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, `owner` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, - `assignees` text COLLATE utf8mb4_general_ci DEFAULT NULL, + `assignees` json DEFAULT NULL, `cr_req` int NOT NULL DEFAULT '2', `qa_req` int NOT NULL DEFAULT '1', `date` int unsigned DEFAULT NULL, diff --git a/models/db_pull.js b/models/db_pull.js index 9c6cfc84..bc63fac5 100644 --- a/models/db_pull.js +++ b/models/db_pull.js @@ -25,6 +25,7 @@ function DBPull(pull) { head_sha: pullData.head.sha, base_branch: pullData.base.ref, owner: getLogin(pullData.user), + // Serialize to a JSON string; MySQL parses it into the `assignees` JSON column. assignees: JSON.stringify(pullData.assignees || []), cr_req: pullData.cr_req, qa_req: pullData.qa_req, diff --git a/models/pull.js b/models/pull.js index 75df9b72..5820f53b 100644 --- a/models/pull.js +++ b/models/pull.js @@ -281,7 +281,8 @@ class Pull { user: { login: data.owner, }, - assignees: data.assignees ? JSON.parse(data.assignees) : [], + // mysql2 auto-parses the JSON column, so this is already an array (or null). + assignees: data.assignees ?? [], cr_req: data.cr_req, qa_req: data.qa_req, }; From cd341d30e82faaa2a15f62ef612cd9950611b00d Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 17:14:30 -0700 Subject: [PATCH 5/6] Record requested_reviewers and rework the assignee/reviewer UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data pipeline (mirrors assignees): capture requested_reviewers from the GitHub payload, persist to a new JSON column (migration 0021), and emit to the frontend. UI: treat assignees as additional authors — blue star when you are the author or an assignee, otherwise the first author's avatar with all authors listed on hover. Surface requested_reviewers as a blue star before the "CR" text when a review is requested from you, whose hover lists all requested reviewers. The participants indicator reverts to participants-only. Expand the pull-card demo with Authors and ReviewRequests examples and give the dummy data assignees/requested_reviewers. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/dummy-pulls.json | 3 + frontend/src/pull-card/avatar.tsx | 4 +- frontend/src/pull-card/index.tsx | 17 +++-- frontend/src/pull-card/participants.tsx | 46 ++++--------- frontend/src/pull-card/signatures.tsx | 13 +++- frontend/src/pull.ts | 15 ++++- frontend/src/theme/day_theme.less | 2 - frontend/src/theme/night_theme.less | 2 - frontend/src/types.ts | 1 + frontend/test/named-pulls.ts | 66 +++++++++++++++++++ frontend/test/pull-card-demo.tsx | 4 ++ frontend/test/pull-data-parts.ts | 2 + .../0021-pulls--add-requested-reviewers.sql | 1 + migrations/schema.sql | 1 + models/db_pull.js | 3 +- models/pull.js | 4 ++ 16 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 migrations/0021-pulls--add-requested-reviewers.sql diff --git a/frontend/dummy-pulls.json b/frontend/dummy-pulls.json index 68059c66..7e84c94b 100644 --- a/frontend/dummy-pulls.json +++ b/frontend/dummy-pulls.json @@ -30,6 +30,7 @@ "login": "BaseInfinity" }, "assignees": ["danielbeardsley"], + "requested_reviewers": ["ianrohde", "danielbeardsley"], "cr_req": 2, "qa_req": 1, "status": { @@ -173,6 +174,7 @@ "login": "ianrohde" }, "assignees": ["danielbeardsley", "ianrohde"], + "requested_reviewers": ["rjmccluskey"], "cr_req": 2, "qa_req": 1, "status": { @@ -612,6 +614,7 @@ "login": "ardelato" }, "cr_req": 2, + "requested_reviewers": ["danielbeardsley", "hackalot805"], "qa_req": 1, "status": { "qa_req": 1, diff --git a/frontend/src/pull-card/avatar.tsx b/frontend/src/pull-card/avatar.tsx index 5913c3ce..1cfc7f89 100644 --- a/frontend/src/pull-card/avatar.tsx +++ b/frontend/src/pull-card/avatar.tsx @@ -4,9 +4,11 @@ import { userProfileUrl } from "../utils"; export function Avatar({ user, linkToProfile, + title, }: { user: string; linkToProfile?: boolean; + title?: string; }) { const cleanUsername = user.replace(/\[bot\]$/, ""); return ( @@ -20,7 +22,7 @@ export function Avatar({ display="inline-block" borderRadius="full" verticalAlign="bottom" - title={user} + title={title ?? user} src={`https://github.com/${cleanUsername}.png?size=20`} /> ); diff --git a/frontend/src/pull-card/index.tsx b/frontend/src/pull-card/index.tsx index cf8aa21d..64c906d7 100644 --- a/frontend/src/pull-card/index.tsx +++ b/frontend/src/pull-card/index.tsx @@ -79,14 +79,18 @@ export const PullCard = memo(function PullCard({ isExternal color="var(--pull-title)" > - {pull.isMine() ? ( + {pull.authoredByMe() ? ( ) : ( - + )} {pull.getRepoName()} #{pull.number}:{" "} @@ -120,6 +124,7 @@ export const PullCard = memo(function PullCard({ signatures={pull.cr_signatures} required={pull.status.cr_req} title="CR" + showReviewRequest /> - {pull.isMine() ? ( + {pull.authoredByMe() ? ( ) : ( - + )} {pull.getRepoName()} #{pull.number}:{" "} diff --git a/frontend/src/pull-card/participants.tsx b/frontend/src/pull-card/participants.tsx index c6b2ca42..c9dff32f 100644 --- a/frontend/src/pull-card/participants.tsx +++ b/frontend/src/pull-card/participants.tsx @@ -8,51 +8,29 @@ import { faUsers, } from "@fortawesome/free-solid-svg-icons"; -// A single indicator whose shape encodes the number of participants -// (one / many / none-is-blank) and whose color encodes the current user's -// relationship to the pull: assigned, participating, or neither. export function Participants({ pull }: { pull: Pull }) { - const count = pull.participants?.length ?? 0; - if (!count && !pull.assignedToMe()) { + if (!pull.participants?.length) { return null; } return ( 1 ? faUsers : faUser} + icon={pull.participants.length > 1 ? faUsers : faUser} title={tooltip(pull)} - color={color(pull)} + color={pull.participating() ? "var(--participants-including-me)" : "var(--participants-without-me)"} /> ); } -function color(pull: Pull) { - if (pull.assignedToMe()) { - return "var(--assignee-icon)"; - } - return pull.participating() - ? "var(--participants-including-me)" - : "var(--participants-without-me)"; -} - function tooltip(pull: Pull) { - const count = pull.participants?.length ?? 0; - const participants = - count === 0 - ? null - : count === 1 - ? pull.participating() - ? "only you participating" - : "1 participant" - : pull.participating() - ? `${count} participants (including you)` - : `${count} participants`; - - if (pull.assignedToMe()) { - return participants ? `Assigned to you, ${participants}` : "Assigned to you"; - } - return participants - ? participants.charAt(0).toUpperCase() + participants.slice(1) - : ""; + if (pull.participants.length == 1) { + return pull.participating() ? + "Only you participating" : + "1 participant" + } else { + return pull.participating() ? + `${pull.participants.length} participants (including you)` : + `${pull.participants.length} participants`; + } } diff --git a/frontend/src/pull-card/signatures.tsx b/frontend/src/pull-card/signatures.tsx index 8bab424b..992cda87 100644 --- a/frontend/src/pull-card/signatures.tsx +++ b/frontend/src/pull-card/signatures.tsx @@ -18,13 +18,16 @@ import { } from "@chakra-ui/react"; import { memo } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { faCheckCircle, faStar } from "@fortawesome/free-solid-svg-icons"; interface SignaturesProps { pull: Pull; title: string; signatures: SignatureGroup; required: number; + // When set (the CR block), show a blue star before the title if a review + // is requested from the current user. + showReviewRequest?: boolean; } const SigSectionTitle = Box; @@ -34,6 +37,7 @@ function SignaturesFlag({ title, signatures, required, + showReviewRequest, }: SignaturesProps) { const statusVariant = signatures.current.length >= required @@ -59,6 +63,13 @@ function SignaturesFlag({ sx={styles} title={noneToShow ? `No ${title} Required` : ""} > + {showReviewRequest && pull.reviewRequestedFromMe() && ( + + )} {title} diff --git a/frontend/src/pull.ts b/frontend/src/pull.ts index 8e7863c8..75baa813 100644 --- a/frontend/src/pull.ts +++ b/frontend/src/pull.ts @@ -84,8 +84,19 @@ export class Pull extends PullData { return this.participants && this.participants.includes(getUser()); } - assignedToMe(): boolean { - return this.assignees?.includes(getUser()) ?? false; + // The pull author plus its assignees, treated as a combined author list. + authors(): string[] { + return [this.user.login, ...(this.assignees ?? [])].filter( + (login, i, all) => all.indexOf(login) === i + ); + } + + authoredByMe(): boolean { + return this.authors().includes(getUser()); + } + + reviewRequestedFromMe(): boolean { + return this.requested_reviewers?.includes(getUser()) ?? false; } hasMyDevBlock(): boolean { diff --git a/frontend/src/theme/day_theme.less b/frontend/src/theme/day_theme.less index 7926a90f..cd9887be 100644 --- a/frontend/src/theme/day_theme.less +++ b/frontend/src/theme/day_theme.less @@ -178,8 +178,6 @@ body[data-theme="day_theme"] { --participants-without-me: @grey; --participants-including-me: @blue; - --assignee-icon: darken(@green, 10%); - // Lines Changed --additions: darken(@green, 10%); --deletions: @red; diff --git a/frontend/src/theme/night_theme.less b/frontend/src/theme/night_theme.less index de7a2e1c..4f29943c 100644 --- a/frontend/src/theme/night_theme.less +++ b/frontend/src/theme/night_theme.less @@ -171,8 +171,6 @@ body[data-theme="night_theme"] { --participants-without-me: darken(@grey, 15%); --participants-including-me: @blue; - --assignee-icon: @green; - --copy-branch: darken(@grey, 15%); --copy-branch-hover: @blue; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2f046620..bad323fe 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -126,4 +126,5 @@ export class PullData { labels: Label[]; participants: string[]; assignees: string[]; + requested_reviewers: string[]; } diff --git a/frontend/test/named-pulls.ts b/frontend/test/named-pulls.ts index 428ffb1c..542ce12e 100644 --- a/frontend/test/named-pulls.ts +++ b/frontend/test/named-pulls.ts @@ -423,6 +423,70 @@ export const Participants = [ }), ]; +export const Authors = [ + pullData({ + number: 1, + title: "Single author (not me)", + cr_req: 0, + user: { login: "author1" }, + }), + pullData({ + number: 2, + title: "Author plus two assignees (none is me) — hover lists all", + cr_req: 0, + user: { login: "author1" }, + assignees: ["assignee1", "assignee2"], + }), + pullData({ + number: 3, + title: "Authored by me", + cr_req: 0, + user: { login: getUser() }, + }), + pullData({ + number: 4, + title: "Assigned to me (I'm an assignee, not the author)", + cr_req: 0, + user: { login: "author1" }, + assignees: [getUser()], + }), +]; + +export const ReviewRequests = [ + pullData({ + number: 1, + title: "Review requested from me", + cr_req: 1, + requested_reviewers: [getUser()], + }), + pullData({ + number: 1, + title: "Review requested from me", + cr_req: 1, + requested_reviewers: [getUser()], + status: { + allQA: [], + allCR: [sig(SignatureType.CR, true, getUser())], + }, + }), + pullData({ + number: 2, + title: "Review requested from me and another", + cr_req: 1, + requested_reviewers: [getUser(), "User 2"], + status: { + allQA: [], + allCR: [sig(SignatureType.CR, true, "User 2")], + }, + }), + pullData({ + number: 3, + title: "Review requested from someone else", + cr_req: 1, + requested_reviewers: ["User2"], + }), +]; + export const KitchenSink = [ pullData({ title: "Pull With Lots of flags and such and a really long title", @@ -462,5 +526,7 @@ export const KitchenSink = [ }), ], participants: ["other person", getUser()], + assignees: ["coAuthor"], + requested_reviewers: [getUser()], }), ]; diff --git a/frontend/test/pull-card-demo.tsx b/frontend/test/pull-card-demo.tsx index c89c4731..07031e1d 100644 --- a/frontend/test/pull-card-demo.tsx +++ b/frontend/test/pull-card-demo.tsx @@ -17,6 +17,8 @@ import { Labels, Draft, MyOwn, + Authors, + ReviewRequests, Participants, KitchenSink, } from "./named-pulls"; @@ -51,6 +53,8 @@ function PullCardDemo() { + + diff --git a/frontend/test/pull-data-parts.ts b/frontend/test/pull-data-parts.ts index a0d57186..5ead8a1a 100644 --- a/frontend/test/pull-data-parts.ts +++ b/frontend/test/pull-data-parts.ts @@ -114,6 +114,8 @@ export function pullData(p: DeepPartial): PullData { }, labels: p.labels || [], participants: p.participants || [], + assignees: p.assignees || [], + requested_reviewers: p.requested_reviewers || [], }; } diff --git a/migrations/0021-pulls--add-requested-reviewers.sql b/migrations/0021-pulls--add-requested-reviewers.sql new file mode 100644 index 00000000..e820dbab --- /dev/null +++ b/migrations/0021-pulls--add-requested-reviewers.sql @@ -0,0 +1 @@ +ALTER TABLE `pulls` ADD COLUMN `requested_reviewers` json DEFAULT NULL AFTER `assignees`; diff --git a/migrations/schema.sql b/migrations/schema.sql index da2919b9..824677a0 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -141,6 +141,7 @@ CREATE TABLE IF NOT EXISTS `pulls` ( `base_branch` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, `owner` varchar(255) COLLATE utf8mb4_general_ci NOT NULL, `assignees` json DEFAULT NULL, + `requested_reviewers` json DEFAULT NULL, `cr_req` int NOT NULL DEFAULT '2', `qa_req` int NOT NULL DEFAULT '1', `date` int unsigned DEFAULT NULL, diff --git a/models/db_pull.js b/models/db_pull.js index bc63fac5..0e658037 100644 --- a/models/db_pull.js +++ b/models/db_pull.js @@ -25,8 +25,9 @@ function DBPull(pull) { head_sha: pullData.head.sha, base_branch: pullData.base.ref, owner: getLogin(pullData.user), - // Serialize to a JSON string; MySQL parses it into the `assignees` JSON column. + // Serialize to a JSON string; MySQL parses it into the JSON column. assignees: JSON.stringify(pullData.assignees || []), + requested_reviewers: JSON.stringify(pullData.requested_reviewers || []), cr_req: pullData.cr_req, qa_req: pullData.qa_req, closes: pullData.closes, diff --git a/models/pull.js b/models/pull.js index 5820f53b..9f4da3f7 100644 --- a/models/pull.js +++ b/models/pull.js @@ -226,6 +226,9 @@ class Pull { login: getLogin(data.user), }, assignees: (data.assignees || []).map((a) => getLogin(a)), + requested_reviewers: (data.requested_reviewers || []).map((r) => + getLogin(r) + ), commits: data.commits, additions: data.additions, deletions: data.deletions, @@ -283,6 +286,7 @@ class Pull { }, // mysql2 auto-parses the JSON column, so this is already an array (or null). assignees: data.assignees ?? [], + requested_reviewers: data.requested_reviewers ?? [], cr_req: data.cr_req, qa_req: data.qa_req, }; From 40a4a0393924c7ed99b162c761516bd186c743bf Mon Sep 17 00:00:00 2001 From: Daniel Beardsley Date: Wed, 15 Jul 2026 17:22:45 -0700 Subject: [PATCH 6/6] Include assignee and requested-reviewer pulls in Personal View Extend isMineViaAffiliation to cover pulls where you are an assignee (via authoredByMe) or a requested reviewer, so Personal View surfaces them alongside your authored/signed/blocked/participating pulls. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/pull.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pull.ts b/frontend/src/pull.ts index 75baa813..8014350b 100644 --- a/frontend/src/pull.ts +++ b/frontend/src/pull.ts @@ -45,7 +45,8 @@ export class Pull extends PullData { isMineViaAffiliation(): boolean { return ( - this.isMine() || + this.authoredByMe() || + this.reviewRequestedFromMe() || this.hasCurrentSig(getUser()) || this.hasOutdatedSig(getUser()) || this.hasMyDevBlock() ||