From 76ff8ab45ce9f4465ca41776c162d4f04a343440 Mon Sep 17 00:00:00 2001 From: Vincent Primault Date: Sat, 2 Nov 2024 18:45:22 +0100 Subject: [PATCH] feat: add a button to copy pull URL (#118) Since links in the table are not real links, it is not possible to copy the URL using a right click. This adds a button to copy the URL to the clipboard. This button appears when hovering a row. --- .../src/components/CopyToClipboardIcon.tsx | 27 +++ .../webapp/src/components/IconWithTooltip.tsx | 21 ++- .../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 | 160 +----------------- 6 files changed, 278 insertions(+), 218 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..7c7935c --- /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 ( + handleClick(e)} + /> + ); +} diff --git a/apps/webapp/src/components/IconWithTooltip.tsx b/apps/webapp/src/components/IconWithTooltip.tsx index 8bd16a4..3c7a141 100644 --- a/apps/webapp/src/components/IconWithTooltip.tsx +++ b/apps/webapp/src/components/IconWithTooltip.tsx @@ -1,16 +1,33 @@ 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 ( - + ); } 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..c96c59e --- /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 handleStar = (e: React.MouseEvent) => { + e.stopPropagation(); + onStar && onStar(); + }; + return ( + handleClick(e)} + onMouseEnter={() => setActive(true)} + onMouseLeave={() => setActive(false)} + className={styles.row} + > + handleStar(e)}> + {pull.starred ? ( + + ) : ( + + )} + + + {pull.attention?.set && ( + + )} + + +
+ + {pull.author.avatarUrl ? ( + + ) : ( + + )} + +
+ + + {pull.state == PullState.Draft ? ( + + ) : pull.state == PullState.Merged ? ( + + ) : pull.state == PullState.Closed ? ( + + ) : pull.state == PullState.Approved ? ( + + ) : pull.state == PullState.Pending ? ( + + ) : null} + + + {pull.ciState == CheckState.Error ? ( + + ) : pull.ciState == CheckState.Failure ? ( + + ) : pull.ciState == CheckState.Success ? ( + + ) : pull.ciState == CheckState.Pending ? ( + + ) : ( + + )} + + + + + + + + + +{pull.additions} /{" "} + -{pull.deletions} + + } + openOnTargetFocus={false} + usePortal={false} + > + {computeSize(pull)} + + + +
+ {pull.title} + {active && ( + + )} +
+
+ {pull.host}/{pull.repo} #{pull.number} +
+ + + ); +} 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..1631fcd 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

No results

; } - 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 ( @@ -57,131 +33,11 @@ export default function PullTable({ pulls, onStar }: Props) { {pulls.map((pull, idx) => ( - handleClick(e, pull)}> - handleStar(e, pull)}> - {pull.starred ? ( - - ) : ( - - )} - - - {pull.attention?.set && ( - - )} - - -
- - {pull.author.avatarUrl ? ( - - ) : ( - - )} - -
- - - {pull.state == PullState.Draft ? ( - - ) : pull.state == PullState.Merged ? ( - - ) : pull.state == PullState.Closed ? ( - - ) : pull.state == PullState.Approved ? ( - - ) : pull.state == PullState.Pending ? ( - - ) : null} - - - {pull.ciState == CheckState.Error ? ( - - ) : pull.ciState == CheckState.Failure ? ( - - ) : pull.ciState == CheckState.Success ? ( - - ) : pull.ciState == CheckState.Pending ? ( - - ) : ( - - )} - - - - - - - - - +{pull.additions}{" "} - /{" "} - -{pull.deletions} - - } - openOnTargetFocus={false} - usePortal={false} - > - {computeSize(pull)} - - - -
{pull.title}
-
- {pull.host}/{pull.repo} #{pull.number} -
- - + onStar && onStar(pull)} + /> ))}