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, };