Skip to content

Commit

Permalink
feat: add a button to copy pull URL (#118)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pvcnt authored Nov 2, 2024
1 parent 652f0fb commit 76ff8ab
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 218 deletions.
27 changes: 27 additions & 0 deletions apps/webapp/src/components/CopyToClipboardIcon.tsx
Original file line number Diff line number Diff line change
@@ -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)}
/>
);
}
21 changes: 19 additions & 2 deletions apps/webapp/src/components/IconWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip content={title} openOnTargetFocus={false} usePortal={false}>
<Icon icon={icon} color={color} />
<Icon
icon={icon}
color={color}
size={size}
className={className}
onClick={onClick}
/>
</Tooltip>
);
}
75 changes: 75 additions & 0 deletions apps/webapp/src/components/PullRow.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
147 changes: 147 additions & 0 deletions apps/webapp/src/components/PullRow.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<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>
);
}
66 changes: 2 additions & 64 deletions apps/webapp/src/components/PullTable.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,17 @@

.table {
width: 100%;
thead th {
th {
color: $pt-text-color-muted !important;
font-weight: normal !important;
font-size: $pt-font-size-small;
svg {
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;
}
}
Loading

0 comments on commit 76ff8ab

Please sign in to comment.