From 6750ccf7c737e6e406856ed86397a74b3fa7088f Mon Sep 17 00:00:00 2001 From: Vincent <vincent.primault@gmail.com> Date: Sat, 2 Nov 2024 18:40:02 +0100 Subject: [PATCH 1/3] feat: add a button to copy pull URL --- .../src/components/CopyToClipboardIcon.tsx | 27 +++ .../webapp/src/components/IconWithTooltip.tsx | 15 +- .../webapp/src/components/PullRow.module.scss | 75 +++++++++ apps/webapp/src/components/PullRow.tsx | 147 ++++++++++++++++ .../src/components/PullTable.module.scss | 66 +------- apps/webapp/src/components/PullTable.tsx | 158 +----------------- 6 files changed, 268 insertions(+), 220 deletions(-) create mode 100644 apps/webapp/src/components/CopyToClipboardIcon.tsx create mode 100644 apps/webapp/src/components/PullRow.module.scss create mode 100644 apps/webapp/src/components/PullRow.tsx diff --git a/apps/webapp/src/components/CopyToClipboardIcon.tsx b/apps/webapp/src/components/CopyToClipboardIcon.tsx new file mode 100644 index 0000000..0dba268 --- /dev/null +++ b/apps/webapp/src/components/CopyToClipboardIcon.tsx @@ -0,0 +1,27 @@ +import IconWithTooltip from "./IconWithTooltip"; +import { useState } from "react"; + +export type Props = { + text: string; + title: string; + className?: string; +}; + +export default function CopyToClipboardIcon({ text, title, className }: Props) { + const [clicked, setClicked] = useState(false); + const handleClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + await navigator.clipboard.writeText(text); + setClicked(true); + setTimeout(() => setClicked(false), 1500); + }; + return ( + <IconWithTooltip + title={clicked ? "Copied!" : title} + icon={clicked ? "tick" : "clipboard"} + className={className} + size={14} + onClick={(e) => handleClick(e)} + /> + ); +} diff --git a/apps/webapp/src/components/IconWithTooltip.tsx b/apps/webapp/src/components/IconWithTooltip.tsx index 8bd16a4..9330135 100644 --- a/apps/webapp/src/components/IconWithTooltip.tsx +++ b/apps/webapp/src/components/IconWithTooltip.tsx @@ -1,16 +1,27 @@ import { Icon, Tooltip } from "@blueprintjs/core"; import { BlueprintIcons_16Id } from "@blueprintjs/icons/lib/esm/generated/16px/blueprint-icons-16"; +import React from "react"; type Props = { icon: BlueprintIcons_16Id; title: string; + className?: string; color?: string; + size?: number; + onClick?: React.MouseEventHandler; }; -export default function IconWithTooltip({ icon, title, color }: Props) { +export default function IconWithTooltip({ + icon, + title, + className, + color, + size, + onClick, +}: Props) { return ( <Tooltip content={title} openOnTargetFocus={false} usePortal={false}> - <Icon icon={icon} color={color} /> + <Icon icon={icon} color={color} size={size} className={className} onClick={onClick} /> </Tooltip> ); } diff --git a/apps/webapp/src/components/PullRow.module.scss b/apps/webapp/src/components/PullRow.module.scss new file mode 100644 index 0000000..c452652 --- /dev/null +++ b/apps/webapp/src/components/PullRow.module.scss @@ -0,0 +1,75 @@ +@import "@blueprintjs/core/lib/scss/variables"; + +.row { + td { + overflow: hidden; + vertical-align: middle !important; + } + td:nth-child(1) { /* Star */ + text-align: center; + width: 25px; + padding: 0.5rem; + } + td:nth-child(2) { /* Attention */ + text-align: center; + width: 25px; + padding: 0.5rem; + } + td:nth-child(3) { /* Author */ + text-align: center; + width: 50px; + padding: 0.5rem; + } + td:nth-child(4) { /* Status */ + text-align: center; + width: 25px; + padding: 0.5rem; + } + td:nth-child(5) { /* CI Status */ + text-align: center; + width: 25px; + padding: 0.5rem; + } + td:nth-child(6) { /* Last Action */ + width: 160px; + } + td:nth-child(7) { /* Size */ + text-align: center; + width: 50px; + } +} + +.title { + margin-bottom: 0.25 * $pt-grid-size; + span { + font-weight: 600; + } +} + +.source { + font-size: $pt-font-size-small; +} + +.author img { + border-radius: 5rem; + height: 32px; +} + +.status { + display: flex; + gap: 0.5rem; +} + +.additions { + color: $green4 !important; +} + +.deletions { + color: $red4 !important; +} + +.copy { + color: $gray1; + padding-left: $pt-grid-size; + padding-right: $pt-grid-size; +} \ No newline at end of file diff --git a/apps/webapp/src/components/PullRow.tsx b/apps/webapp/src/components/PullRow.tsx new file mode 100644 index 0000000..dccd3e3 --- /dev/null +++ b/apps/webapp/src/components/PullRow.tsx @@ -0,0 +1,147 @@ +import { Tooltip, Tag, Icon } from "@blueprintjs/core"; +import TimeAgo from "./TimeAgo"; +import { CheckState, type Pull, PullState } from "@repo/model"; +import IconWithTooltip from "./IconWithTooltip"; +import { computeSize } from "../lib/size"; +import styles from "./PullRow.module.scss"; +import { useState } from "react"; +import CopyToClipboardIcon from "./CopyToClipboardIcon"; + +export type Props = { + pull: Pull; + onStar?: () => void; +}; + +const formatDate = (d: Date | string) => { + return new Date(d).toLocaleDateString("en", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +}; + +export default function PullRow({ pull, onStar }: Props) { + const [active, setActive] = useState(false); + const handleClick = (e: React.MouseEvent) => { + // Manually reproduce the behaviour of CTRL+click or middle mouse button. + if (e.metaKey || e.ctrlKey || e.button == 1) { + window.open(pull.url); + } else { + window.location.href = pull.url; + } + }; + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); + await navigator.clipboard.writeText(pull.url); + }; + const handleStar = (e: React.MouseEvent) => { + e.stopPropagation(); + onStar && onStar(); + }; + return ( + <tr + onClick={(e) => handleClick(e)} + onMouseEnter={() => setActive(true)} + onMouseLeave={() => setActive(false)} + className={styles.row} + > + <td onClick={(e) => handleStar(e)}> + {pull.starred ? ( + <IconWithTooltip + icon="star" + color="#FBD065" + title="Unstar pull request" + /> + ) : ( + <IconWithTooltip icon="star-empty" title="Star pull request" /> + )} + </td> + <td> + {pull.attention?.set && ( + <IconWithTooltip + icon="flag" + color="#CD4246" + title={`You are in the attention set: ${pull.attention?.reason}`} + /> + )} + </td> + <td> + <div className={styles.author}> + <Tooltip content={pull.author.name}> + {pull.author.avatarUrl ? ( + <img src={pull.author.avatarUrl} /> + ) : ( + <Icon icon="user" /> + )} + </Tooltip> + </div> + </td> + <td> + {pull.state == PullState.Draft ? ( + <IconWithTooltip icon="document" title="Draft" color="#5F6B7C" /> + ) : pull.state == PullState.Merged ? ( + <IconWithTooltip icon="git-merge" title="Merged" color="#634DBF" /> + ) : pull.state == PullState.Closed ? ( + <IconWithTooltip icon="cross-circle" title="Closed" color="#AC2F33" /> + ) : pull.state == PullState.Approved ? ( + <IconWithTooltip icon="git-pull" title="Approved" color="#1C6E42" /> + ) : pull.state == PullState.Pending ? ( + <IconWithTooltip icon="git-pull" title="Pending" color="#C87619" /> + ) : null} + </td> + <td> + {pull.ciState == CheckState.Error ? ( + <IconWithTooltip icon="error" title="Error" color="#AC2F33" /> + ) : pull.ciState == CheckState.Failure ? ( + <IconWithTooltip + icon="cross-circle" + title="Some checks are failing" + color="#AC2F33" + /> + ) : pull.ciState == CheckState.Success ? ( + <IconWithTooltip + icon="tick-circle" + title="All checks passing" + color="#1C6E42" + /> + ) : pull.ciState == CheckState.Pending ? ( + <IconWithTooltip icon="circle" title="Pending" color="#C87619" /> + ) : ( + <IconWithTooltip icon="remove" title="No status" color="#5F6B7C" /> + )} + </td> + <td> + <Tooltip content={formatDate(pull.updatedAt)}> + <TimeAgo date={pull.updatedAt} tooltip={false} timeStyle="round" /> + </Tooltip> + </td> + <td> + <Tooltip + content={ + <> + <span className={styles.additions}>+{pull.additions}</span> /{" "} + <span className={styles.deletions}>-{pull.deletions}</span> + </> + } + openOnTargetFocus={false} + usePortal={false} + > + <Tag>{computeSize(pull)}</Tag> + </Tooltip> + </td> + <td> + <div className={styles.title}> + <span>{pull.title}</span> + {active && ( + <CopyToClipboardIcon title="Copy URL to clipboard" text={pull.url} className={styles.copy}/> + )} + </div> + <div className={styles.source}> + {pull.host}/{pull.repo} #{pull.number} + </div> + </td> + </tr> + ); +} diff --git a/apps/webapp/src/components/PullTable.module.scss b/apps/webapp/src/components/PullTable.module.scss index 9b016ba..25f9f07 100644 --- a/apps/webapp/src/components/PullTable.module.scss +++ b/apps/webapp/src/components/PullTable.module.scss @@ -2,7 +2,7 @@ .table { width: 100%; - thead th { + th { color: $pt-text-color-muted !important; font-weight: normal !important; font-size: $pt-font-size-small; @@ -10,71 +10,9 @@ fill: $pt-text-color-muted; } } - tbody td { - overflow: hidden; - vertical-align: middle !important; - } - tbody td:nth-child(1) { /* Star */ - text-align: center; - width: 25px; - padding: 0.5rem; - } - tbody td:nth-child(2) { /* Attention */ - text-align: center; - width: 25px; - padding: 0.5rem; - } - tbody td:nth-child(3) { /* Author */ - text-align: center; - width: 50px; - padding: 0.5rem; - } - tbody td:nth-child(4) { /* Status */ - text-align: center; - width: 25px; - padding: 0.5rem; - } - tbody td:nth-child(5) { /* CI Status */ - text-align: center; - width: 25px; - padding: 0.5rem; - } - tbody td:nth-child(6) { /* Last Action */ - width: 160px; - } - tbody td:nth-child(7) { /* Size */ - text-align: center; - width: 50px; - } -} - -.title { - font-weight: 600; -} - -.source { - font-size: $pt-font-size-small; -} - -.author img { - border-radius: 5rem; - height: 32px; -} - -.status { - display: flex; - gap: 0.5rem; -} - -.additions { - color: $green4 !important; -} - -.deletions { - color: $red4 !important; } .empty { color: $dark-gray5; margin: 0; -} +} \ No newline at end of file diff --git a/apps/webapp/src/components/PullTable.tsx b/apps/webapp/src/components/PullTable.tsx index 00c3c29..b6feb0c 100644 --- a/apps/webapp/src/components/PullTable.tsx +++ b/apps/webapp/src/components/PullTable.tsx @@ -1,9 +1,7 @@ -import { HTMLTable, Tooltip, Tag, Icon } from "@blueprintjs/core"; -import TimeAgo from "./TimeAgo"; -import { CheckState, type Pull, PullState } from "@repo/model"; +import { HTMLTable } from "@blueprintjs/core"; +import { type Pull } from "@repo/model"; import IconWithTooltip from "./IconWithTooltip"; -import { computeSize } from "../lib/size"; - +import PullRow from "./PullRow"; import styles from "./PullTable.module.scss"; export type Props = { @@ -11,32 +9,10 @@ export type Props = { onStar?: (v: Pull) => void; }; -const formatDate = (d: Date | string) => { - return new Date(d).toLocaleDateString("en", { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - }); -}; - export default function PullTable({ pulls, onStar }: Props) { if (pulls.length === 0) { return <p className={styles.empty}>No results</p>; } - const handleClick = (e: React.MouseEvent, pull: Pull) => { - // Manually reproduce the behaviour of CTRL+click or middle mouse button. - if (e.metaKey || e.ctrlKey || e.button == 1) { - window.open(pull.url); - } else { - window.location.href = pull.url; - } - }; - const handleStar = (e: React.MouseEvent, pull: Pull) => { - onStar && onStar(pull); - e.stopPropagation(); - }; return ( <HTMLTable interactive className={styles.table}> <thead> @@ -56,133 +32,7 @@ export default function PullTable({ pulls, onStar }: Props) { </tr> </thead> <tbody> - {pulls.map((pull, idx) => ( - <tr key={idx} onClick={(e) => handleClick(e, pull)}> - <td onClick={(e) => handleStar(e, pull)}> - {pull.starred ? ( - <IconWithTooltip - icon="star" - color="#FBD065" - title="Unstar pull request" - /> - ) : ( - <IconWithTooltip icon="star-empty" title="Star pull request" /> - )} - </td> - <td> - {pull.attention?.set && ( - <IconWithTooltip - icon="flag" - color="#CD4246" - title={`You are in the attention set: ${pull.attention?.reason}`} - /> - )} - </td> - <td> - <div className={styles.author}> - <Tooltip content={pull.author.name}> - {pull.author.avatarUrl ? ( - <img src={pull.author.avatarUrl} /> - ) : ( - <Icon icon="user" /> - )} - </Tooltip> - </div> - </td> - <td> - {pull.state == PullState.Draft ? ( - <IconWithTooltip - icon="document" - title="Draft" - color="#5F6B7C" - /> - ) : pull.state == PullState.Merged ? ( - <IconWithTooltip - icon="git-merge" - title="Merged" - color="#634DBF" - /> - ) : pull.state == PullState.Closed ? ( - <IconWithTooltip - icon="cross-circle" - title="Closed" - color="#AC2F33" - /> - ) : pull.state == PullState.Approved ? ( - <IconWithTooltip - icon="git-pull" - title="Approved" - color="#1C6E42" - /> - ) : pull.state == PullState.Pending ? ( - <IconWithTooltip - icon="git-pull" - title="Pending" - color="#C87619" - /> - ) : null} - </td> - <td> - {pull.ciState == CheckState.Error ? ( - <IconWithTooltip icon="error" title="Error" color="#AC2F33" /> - ) : pull.ciState == CheckState.Failure ? ( - <IconWithTooltip - icon="cross-circle" - title="Some checks are failing" - color="#AC2F33" - /> - ) : pull.ciState == CheckState.Success ? ( - <IconWithTooltip - icon="tick-circle" - title="All checks passing" - color="#1C6E42" - /> - ) : pull.ciState == CheckState.Pending ? ( - <IconWithTooltip - icon="circle" - title="Pending" - color="#C87619" - /> - ) : ( - <IconWithTooltip - icon="remove" - title="No status" - color="#5F6B7C" - /> - )} - </td> - <td> - <Tooltip content={formatDate(pull.updatedAt)}> - <TimeAgo - date={pull.updatedAt} - tooltip={false} - timeStyle="round" - /> - </Tooltip> - </td> - <td> - <Tooltip - content={ - <> - <span className={styles.additions}>+{pull.additions}</span>{" "} - /{" "} - <span className={styles.deletions}>-{pull.deletions}</span> - </> - } - openOnTargetFocus={false} - usePortal={false} - > - <Tag>{computeSize(pull)}</Tag> - </Tooltip> - </td> - <td> - <div className={styles.title}>{pull.title}</div> - <div className={styles.source}> - {pull.host}/{pull.repo} #{pull.number} - </div> - </td> - </tr> - ))} + {pulls.map((pull, idx) => <PullRow key={idx} pull={pull} onStar={() => onStar && onStar(pull)} />)} </tbody> </HTMLTable> ); From 08753e885bd4312f16bee45c906202241d3997e6 Mon Sep 17 00:00:00 2001 From: Vincent <vincent.primault@gmail.com> Date: Sat, 2 Nov 2024 18:41:11 +0100 Subject: [PATCH 2/3] format --- .../src/components/CopyToClipboardIcon.tsx | 26 +++++++++---------- .../webapp/src/components/IconWithTooltip.tsx | 8 +++++- apps/webapp/src/components/PullRow.tsx | 6 ++++- apps/webapp/src/components/PullTable.tsx | 8 +++++- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/apps/webapp/src/components/CopyToClipboardIcon.tsx b/apps/webapp/src/components/CopyToClipboardIcon.tsx index 0dba268..7c7935c 100644 --- a/apps/webapp/src/components/CopyToClipboardIcon.tsx +++ b/apps/webapp/src/components/CopyToClipboardIcon.tsx @@ -3,25 +3,25 @@ import { useState } from "react"; export type Props = { text: string; - title: string; - className?: string; + title: string; + className?: string; }; export default function CopyToClipboardIcon({ text, title, className }: Props) { - const [clicked, setClicked] = useState(false); + const [clicked, setClicked] = useState(false); const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); - await navigator.clipboard.writeText(text); - setClicked(true); - setTimeout(() => setClicked(false), 1500); + await navigator.clipboard.writeText(text); + setClicked(true); + setTimeout(() => setClicked(false), 1500); }; return ( - <IconWithTooltip - title={clicked ? "Copied!" : title} - icon={clicked ? "tick" : "clipboard"} - className={className} - size={14} - onClick={(e) => handleClick(e)} - /> + <IconWithTooltip + title={clicked ? "Copied!" : title} + icon={clicked ? "tick" : "clipboard"} + className={className} + size={14} + onClick={(e) => handleClick(e)} + /> ); } diff --git a/apps/webapp/src/components/IconWithTooltip.tsx b/apps/webapp/src/components/IconWithTooltip.tsx index 9330135..3c7a141 100644 --- a/apps/webapp/src/components/IconWithTooltip.tsx +++ b/apps/webapp/src/components/IconWithTooltip.tsx @@ -21,7 +21,13 @@ export default function IconWithTooltip({ }: Props) { return ( <Tooltip content={title} openOnTargetFocus={false} usePortal={false}> - <Icon icon={icon} color={color} size={size} className={className} onClick={onClick} /> + <Icon + icon={icon} + color={color} + size={size} + className={className} + onClick={onClick} + /> </Tooltip> ); } diff --git a/apps/webapp/src/components/PullRow.tsx b/apps/webapp/src/components/PullRow.tsx index dccd3e3..83774ed 100644 --- a/apps/webapp/src/components/PullRow.tsx +++ b/apps/webapp/src/components/PullRow.tsx @@ -135,7 +135,11 @@ export default function PullRow({ pull, onStar }: Props) { <div className={styles.title}> <span>{pull.title}</span> {active && ( - <CopyToClipboardIcon title="Copy URL to clipboard" text={pull.url} className={styles.copy}/> + <CopyToClipboardIcon + title="Copy URL to clipboard" + text={pull.url} + className={styles.copy} + /> )} </div> <div className={styles.source}> diff --git a/apps/webapp/src/components/PullTable.tsx b/apps/webapp/src/components/PullTable.tsx index b6feb0c..1631fcd 100644 --- a/apps/webapp/src/components/PullTable.tsx +++ b/apps/webapp/src/components/PullTable.tsx @@ -32,7 +32,13 @@ export default function PullTable({ pulls, onStar }: Props) { </tr> </thead> <tbody> - {pulls.map((pull, idx) => <PullRow key={idx} pull={pull} onStar={() => onStar && onStar(pull)} />)} + {pulls.map((pull, idx) => ( + <PullRow + key={idx} + pull={pull} + onStar={() => onStar && onStar(pull)} + /> + ))} </tbody> </HTMLTable> ); From ebe97c14854e4ae4149bd9ee55ec746dbc172c66 Mon Sep 17 00:00:00 2001 From: Vincent <vincent.primault@gmail.com> Date: Sat, 2 Nov 2024 18:43:39 +0100 Subject: [PATCH 3/3] remove unused --- apps/webapp/src/components/PullRow.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/webapp/src/components/PullRow.tsx b/apps/webapp/src/components/PullRow.tsx index 83774ed..c96c59e 100644 --- a/apps/webapp/src/components/PullRow.tsx +++ b/apps/webapp/src/components/PullRow.tsx @@ -32,10 +32,6 @@ export default function PullRow({ pull, onStar }: Props) { window.location.href = pull.url; } }; - const handleCopy = async (e: React.MouseEvent) => { - e.stopPropagation(); - await navigator.clipboard.writeText(pull.url); - }; const handleStar = (e: React.MouseEvent) => { e.stopPropagation(); onStar && onStar();