diff --git a/frontend/dummy-pulls.json b/frontend/dummy-pulls.json
index 3511efd0..7e84c94b 100644
--- a/frontend/dummy-pulls.json
+++ b/frontend/dummy-pulls.json
@@ -29,6 +29,8 @@
"user": {
"login": "BaseInfinity"
},
+ "assignees": ["danielbeardsley"],
+ "requested_reviewers": ["ianrohde", "danielbeardsley"],
"cr_req": 2,
"qa_req": 1,
"status": {
@@ -171,6 +173,8 @@
"user": {
"login": "ianrohde"
},
+ "assignees": ["danielbeardsley", "ianrohde"],
+ "requested_reviewers": ["rjmccluskey"],
"cr_req": 2,
"qa_req": 1,
"status": {
@@ -481,6 +485,7 @@
"user": {
"login": "hackalot805"
},
+ "assignees": ["ardelato"],
"cr_req": 2,
"qa_req": 1,
"status": {
@@ -609,6 +614,7 @@
"login": "ardelato"
},
"cr_req": 2,
+ "requested_reviewers": ["danielbeardsley", "hackalot805"],
"qa_req": 1,
"status": {
"qa_req": 1,
@@ -697,6 +703,7 @@
"user": {
"login": "rjmccluskey"
},
+ "assignees": ["hackalot805", "ardelato"],
"cr_req": 2,
"qa_req": 1,
"status": {
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 d999ee25..c9dff32f 100644
--- a/frontend/src/pull-card/participants.tsx
+++ b/frontend/src/pull-card/participants.tsx
@@ -34,4 +34,3 @@ function tooltip(pull: Pull) {
`${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 38236b2f..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() ||
@@ -84,6 +85,21 @@ export class Pull extends PullData {
return this.participants && this.participants.includes(getUser());
}
+ // 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 {
return this.getDevBlock()?.data.user.login == getUser();
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 75d605c3..bad323fe 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -125,4 +125,6 @@ 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/0020-pulls--add-assignees.sql b/migrations/0020-pulls--add-assignees.sql
new file mode 100644
index 00000000..4cb3b190
--- /dev/null
+++ b/migrations/0020-pulls--add-assignees.sql
@@ -0,0 +1 @@
+ALTER TABLE `pulls` ADD COLUMN `assignees` json DEFAULT NULL AFTER `owner`;
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 b4aa0753..824677a0 100644
--- a/migrations/schema.sql
+++ b/migrations/schema.sql
@@ -140,6 +140,8 @@ 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` 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 f762fd9a..0e658037 100644
--- a/models/db_pull.js
+++ b/models/db_pull.js
@@ -25,6 +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 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 60ed4c2a..9f4da3f7 100644
--- a/models/pull.js
+++ b/models/pull.js
@@ -225,6 +225,10 @@ class Pull {
user: {
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,
@@ -280,6 +284,9 @@ class Pull {
user: {
login: data.owner,
},
+ // 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,
};