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

View File

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

View File

@ -134,7 +134,11 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
Copy your one-time code:&nbsp; Copy your one-time code:&nbsp;
<div css={styles.copyCode}> <div css={styles.copyCode}>
<span css={styles.code}>{externalAuthDevice.user_code}</span> <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> </div>
<br /> <br />
Then open the link below and paste it: 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> </Tooltip>
<CopyButton <CopyButton
text={buildInfo.deployment_id} text={buildInfo.deployment_id}
buttonStyles={css` label="Copy deployment ID"
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
`}
/> />
</div> </div>
)} )}
@ -181,7 +173,7 @@ const GithubStar: FC<SvgIconProps> = (props) => (
fill="currentColor" fill="currentColor"
{...props} {...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> </svg>
); );

View File

@ -5,6 +5,7 @@ import { workspaceQuota } from "api/queries/workspaceQuota";
import type * as TypesGen from "api/typesGenerated"; import type * as TypesGen from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarData } from "components/Avatar/AvatarData";
import { CopyButton } from "components/CopyButton/CopyButton";
import { import {
Topbar, Topbar,
TopbarAvatar, TopbarAvatar,
@ -346,50 +347,57 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
templateDisplayName, templateDisplayName,
}) => { }) => {
return ( return (
<Popover mode="hover"> <div className="flex items-center">
<PopoverTrigger> <Popover mode="hover">
<span css={styles.breadcrumbSegment}> <PopoverTrigger>
<TopbarAvatar src={templateIconUrl} fallback={templateDisplayName} /> <span css={styles.breadcrumbSegment}>
<span css={[styles.breadcrumbText, { fontWeight: 500 }]}> <TopbarAvatar
{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} src={templateIconUrl}
fallback={templateDisplayName} fallback={templateDisplayName}
/> />
}
imgFallbackText={templateDisplayName} <span css={[styles.breadcrumbText, { fontWeight: 500 }]}>
/> {workspaceName}
</HelpTooltipContent> </span>
</Popover> </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>
); );
}; };