feat: show sharing info at bottom of AppLink's tooltip (#21197)

for #19974 

`ShareIcon`, which returns a MUI tooltip, had these problems:

1. There was no Storybook story which rendered the "Open external URL"
tooltip
2. `AppLink` renders a button. If it has tooltip text, this button is
also a Radix `TooltipTrigger`. Since `ShareIcon` is rendered as a child
of the `AppLink` button, `ShareIcon`'s tooltip (a nested tooltip) was
never appearing. I did try turning `ShareIcon` into a Radix tooltip as
well, but I still couldn't get the nested tooltip's text to appear, and
nested tooltips are not very accessible. AFAIK there's no way to focus a
child element of a button.

I've deleted the separate `ShareIcon` component and moved `ShareIcon`'s
tooltip text to the bottom of `AppLink`'s tooltip:

## before

<img width="175" height="121" alt="image"
src="https://github.com/user-attachments/assets/ad17927e-c3d1-499b-83f8-a5832b777305"
/>

^The `UsersIcon` on the right is a MUI tooltip trigger, but it can't
receive focus. It's supposed to show the text "Shared with all
authenticated users" when focused

## after

<img width="228" height="121" alt="image"
src="https://github.com/user-attachments/assets/adc202c1-57cd-4a80-8f94-f7f32897d286"
/>
This commit is contained in:
Andrew Aquino
2025-12-11 13:30:16 -08:00
committed by GitHub
parent 37e8b8946a
commit a87a44412f
3 changed files with 55 additions and 49 deletions
@@ -77,6 +77,19 @@ export const ExternalAppNotInstalled: Story = {
},
};
export const ExternalAppShareable: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
url: "vscode://open",
external: true,
sharing_level: "authenticated",
},
agent: MockWorkspaceAgent,
},
};
export const SharingLevelOwner: Story = {
args: {
workspace: MockWorkspace,
+42 -4
View File
@@ -9,13 +9,19 @@ import {
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useProxy } from "contexts/ProxyContext";
import { CircleAlertIcon } from "lucide-react";
import {
Building2Icon,
CircleAlertIcon,
GlobeIcon,
type LucideIcon,
SquareArrowOutUpRightIcon,
UsersIcon,
} from "lucide-react";
import { isExternalApp, needsSessionToken } from "modules/apps/apps";
import { useAppLink } from "modules/apps/useAppLink";
import { type FC, type ReactNode, useState } from "react";
import { AgentButton } from "../AgentButton";
import { BaseIcon } from "./BaseIcon";
import { ShareIcon } from "./ShareIcon";
export const DisplayAppNameMap: Record<TypesGen.DisplayApp, string> = {
port_forwarding_helper: "Ports",
@@ -115,13 +121,24 @@ export const AppLink: FC<AppLinkProps> = ({
}
const canShare = app.sharing_level !== "owner";
const { shareTooltip, shareIcon: ShareIcon } = canShare
? app.external
? {
shareTooltip: "Open external URL",
shareIcon: SquareArrowOutUpRightIcon,
}
: shareDetails[app.sharing_level]
: {
shareTooltip: null,
shareIcon: null,
};
const button = grouped ? (
<DropdownMenuItem asChild>
<a href={canClick ? link.href : undefined} onClick={link.onClick}>
{icon}
{link.label}
{canShare && <ShareIcon app={app} />}
{ShareIcon && <ShareIcon />}
</a>
</DropdownMenuItem>
) : (
@@ -129,7 +146,7 @@ export const AppLink: FC<AppLinkProps> = ({
<a href={canClick ? link.href : undefined} onClick={link.onClick}>
{icon}
{link.label}
{canShare && <ShareIcon app={app} />}
{ShareIcon && <ShareIcon />}
</a>
</AgentButton>
);
@@ -146,6 +163,7 @@ export const AppLink: FC<AppLinkProps> = ({
{app.tooltip}
</Markdown>
) : null}
{shareTooltip}
</TooltipContent>
</Tooltip>
);
@@ -153,3 +171,23 @@ export const AppLink: FC<AppLinkProps> = ({
return button;
};
const shareDetails: {
[SharingLevel in TypesGen.WorkspaceAppSharingLevel as Exclude<
SharingLevel,
"owner"
>]: { shareTooltip: string; shareIcon: LucideIcon };
} = {
authenticated: {
shareTooltip: "Shared with all authenticated users",
shareIcon: UsersIcon,
},
organization: {
shareTooltip: "Shared with organization members",
shareIcon: Building2Icon,
},
public: {
shareTooltip: "Shared publicly",
shareIcon: GlobeIcon,
},
};
@@ -1,45 +0,0 @@
import Tooltip from "@mui/material/Tooltip";
import type * as TypesGen from "api/typesGenerated";
import {
Building2Icon,
GlobeIcon,
SquareArrowOutUpRightIcon,
UsersIcon,
} from "lucide-react";
interface ShareIconProps {
app: TypesGen.WorkspaceApp;
}
export const ShareIcon = ({ app }: ShareIconProps) => {
if (app.external) {
return (
<Tooltip title="Open external URL">
<SquareArrowOutUpRightIcon />
</Tooltip>
);
}
if (app.sharing_level === "authenticated") {
return (
<Tooltip title="Shared with all authenticated users">
<UsersIcon />
</Tooltip>
);
}
if (app.sharing_level === "organization") {
return (
<Tooltip title="Shared with organization members">
<Building2Icon />
</Tooltip>
);
}
if (app.sharing_level === "public") {
return (
<Tooltip title="Shared publicly">
<GlobeIcon />
</Tooltip>
);
}
return null;
};