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();