feat: add copy button for workspace name in breadcrumb (#17822)

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
This commit is contained in:
M Atif Ali
2025-05-15 10:41:01 -07:00
committed by GitHub
parent bbceebde97
commit 2c49fd9e96
6 changed files with 95 additions and 160 deletions

View File

@ -1,6 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import { visuallyHidden } from "@mui/utils";
import { type FC, type KeyboardEvent, type MouseEvent, useRef } from "react";
import type { FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { CopyButton } from "../CopyButton/CopyButton";
@ -21,33 +20,8 @@ export const CodeExample: FC<CodeExampleProps> = ({
// the secure option, not remember to opt in
secret = true,
}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const triggerButton = (event: KeyboardEvent | MouseEvent) => {
const clickTriggeredOutsideButton =
event.target instanceof HTMLElement &&
!buttonRef.current?.contains(event.target);
if (clickTriggeredOutsideButton) {
buttonRef.current?.click();
}
};
return (
<div
css={styles.container}
className={className}
onClick={triggerButton}
onKeyDown={(event) => {
if (event.key === "Enter") {
triggerButton(event);
}
}}
onKeyUp={(event) => {
if (event.key === " ") {
triggerButton(event);
}
}}
>
<div css={styles.container} className={className}>
<code css={[styles.code, secret && styles.secret]}>
{secret ? (
<>
@ -60,7 +34,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
* readily available in the HTML itself
*/}
<span aria-hidden>{obfuscateText(code)}</span>
<span css={{ ...visuallyHidden }}>
<span className="sr-only">
Encrypted text. Please access via the copy button.
</span>
</>
@ -69,7 +43,7 @@ export const CodeExample: FC<CodeExampleProps> = ({
)}
</code>
<CopyButton ref={buttonRef} text={code} />
<CopyButton text={code} label="Copy code" />
</div>
);
};

View File

@ -1,77 +1,44 @@
import { type Interpolation, type Theme, css } from "@emotion/react";
import IconButton from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import { Button, type ButtonProps } from "components/Button/Button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useClipboard } from "hooks/useClipboard";
import { CheckIcon } from "lucide-react";
import { type ReactNode, forwardRef } from "react";
import { FileCopyIcon } from "../Icons/FileCopyIcon";
import { CheckIcon, CopyIcon } from "lucide-react";
import type { FC } from "react";
interface CopyButtonProps {
children?: ReactNode;
type CopyButtonProps = ButtonProps & {
text: string;
ctaCopy?: string;
wrapperStyles?: Interpolation<Theme>;
buttonStyles?: Interpolation<Theme>;
tooltipTitle?: string;
}
const Language = {
tooltipTitle: "Copy to clipboard",
ariaLabel: "Copy to clipboard",
label: string;
};
/**
* Copy button used inside the CodeBlock component internally
*/
export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
(props, ref) => {
const {
text,
ctaCopy,
wrapperStyles,
buttonStyles,
tooltipTitle = Language.tooltipTitle,
} = props;
const { showCopiedSuccess, copyToClipboard } = useClipboard({
textToCopy: text,
});
export const CopyButton: FC<CopyButtonProps> = ({
text,
label,
...buttonProps
}) => {
const { showCopiedSuccess, copyToClipboard } = useClipboard({
textToCopy: text,
});
return (
<Tooltip title={tooltipTitle} placement="top">
<div css={[{ display: "flex" }, wrapperStyles]}>
<IconButton
ref={ref}
css={[styles.button, buttonStyles]}
size="small"
aria-label={Language.ariaLabel}
variant="text"
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="subtle"
onClick={copyToClipboard}
{...buttonProps}
>
{showCopiedSuccess ? (
<CheckIcon css={styles.copyIcon} />
) : (
<FileCopyIcon css={styles.copyIcon} />
)}
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
</IconButton>
</div>
{showCopiedSuccess ? <CheckIcon /> : <CopyIcon />}
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
);
},
);
const styles = {
button: (theme) => css`
border-radius: 8px;
padding: 8px;
min-width: 32px;
&:hover {
background: ${theme.palette.background.paper};
}
`,
copyIcon: css`
width: 20px;
height: 20px;
`,
} satisfies Record<string, Interpolation<Theme>>;
</TooltipProvider>
);
};

View File

@ -134,7 +134,11 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
Copy your one-time code:&nbsp;
<div css={styles.copyCode}>
<span css={styles.code}>{externalAuthDevice.user_code}</span>
&nbsp; <CopyButton text={externalAuthDevice.user_code} />
&nbsp;{" "}
<CopyButton
text={externalAuthDevice.user_code}
label="Copy user code"
/>
</div>
<br />
Then open the link below and paste it:

View File

@ -1,10 +0,0 @@
import SvgIcon, { type SvgIconProps } from "@mui/material/SvgIcon";
export const FileCopyIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 20 20">
<path
d="M12.7412 2.2807H4.32014C3.5447 2.2807 2.91663 2.90877 2.91663 3.68421V13.5088H4.32014V3.68421H12.7412V2.2807ZM14.8465 5.08772H7.12716C6.35172 5.08772 5.72365 5.71579 5.72365 6.49123V16.3158C5.72365 17.0912 6.35172 17.7193 7.12716 17.7193H14.8465C15.6219 17.7193 16.25 17.0912 16.25 16.3158V6.49123C16.25 5.71579 15.6219 5.08772 14.8465 5.08772ZM14.8465 16.3158H7.12716V6.49123H14.8465V16.3158Z"
fill="currentColor"
/>
</SvgIcon>
);

View File

@ -151,15 +151,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
</Tooltip>
<CopyButton
text={buildInfo.deployment_id}
buttonStyles={css`
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
`}
label="Copy deployment ID"
/>
</div>
)}
@ -181,7 +173,7 @@ const GithubStar: FC<SvgIconProps> = (props) => (
fill="currentColor"
{...props}
>
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"></path>
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
</svg>
);

View File

@ -5,6 +5,7 @@ import { workspaceQuota } from "api/queries/workspaceQuota";
import type * as TypesGen from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData";
import { CopyButton } from "components/CopyButton/CopyButton";
import {
Topbar,
TopbarAvatar,
@ -346,50 +347,57 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
templateDisplayName,
}) => {
return (
<Popover mode="hover">
<PopoverTrigger>
<span css={styles.breadcrumbSegment}>
<TopbarAvatar src={templateIconUrl} fallback={templateDisplayName} />
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
{workspaceName}
</span>
</span>
</PopoverTrigger>
<HelpTooltipContent
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
>
<AvatarData
title={
<Link
component={RouterLink}
to={rootTemplateUrl}
css={{ color: "inherit" }}
>
{templateDisplayName}
</Link>
}
subtitle={
<Link
component={RouterLink}
to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`}
css={{ color: "inherit" }}
>
Version: {latestBuildVersionName}
</Link>
}
avatar={
<Avatar
variant="icon"
<div className="flex items-center">
<Popover mode="hover">
<PopoverTrigger>
<span css={styles.breadcrumbSegment}>
<TopbarAvatar
src={templateIconUrl}
fallback={templateDisplayName}
/>
}
imgFallbackText={templateDisplayName}
/>
</HelpTooltipContent>
</Popover>
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
{workspaceName}
</span>
</span>
</PopoverTrigger>
<HelpTooltipContent
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
>
<AvatarData
title={
<Link
component={RouterLink}
to={rootTemplateUrl}
css={{ color: "inherit" }}
>
{templateDisplayName}
</Link>
}
subtitle={
<Link
component={RouterLink}
to={`${rootTemplateUrl}/versions/${encodeURIComponent(templateVersionName)}`}
css={{ color: "inherit" }}
>
Version: {latestBuildVersionName}
</Link>
}
avatar={
<Avatar
variant="icon"
src={templateIconUrl}
fallback={templateDisplayName}
/>
}
imgFallbackText={templateDisplayName}
/>
</HelpTooltipContent>
</Popover>
<CopyButton text={workspaceName} label="Copy workspace name" />
</div>
);
};