Skip to content

Commit 4d11d10

Browse files
nicktrnTrigger.dev RepoOps
authored andcommitted
feat(webapp): admin copy public URL action
Mono-RevId: 30aedb04e1b3e9e8dcff0ad1c1e1313048238e86
1 parent 85ab16b commit 4d11d10

6 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎apps/webapp/app/components/GlobalShortcuts.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useNavigate, useSubmit } from "@remix-run/react";
22
import { useEffect } from "react";
3+
import { useCopyPublicUrl } from "~/hooks/useCopyPublicUrl";
34
import { useIsImpersonating } from "~/hooks/useOrganizations";
45
import { useOptionalUser } from "~/hooks/useUser";
56
import { adminPath } from "~/utils/pathBuilder";
@@ -10,13 +11,21 @@ export function GlobalShortcuts() {
1011
const isImpersonating = useIsImpersonating();
1112
const navigate = useNavigate();
1213
const submit = useSubmit();
14+
const copyPublicUrl = useCopyPublicUrl();
1315

1416
const isAdmin = Boolean(user?.admin) || isImpersonating;
1517

1618
useEffect(() => {
1719
if (!isAdmin) return;
1820

1921
const onKeyDown = (event: KeyboardEvent) => {
22+
if (event.code === "KeyL" && event.altKey && (event.metaKey || event.ctrlKey)) {
23+
if (!copyPublicUrl.enabled) return;
24+
event.preventDefault();
25+
if (!event.repeat) copyPublicUrl.copy();
26+
return;
27+
}
28+
2029
// Admin escape hatch: Cmd+Option+A (Ctrl+Alt+A on Windows) opens the admin dashboard, or stops
2130
// impersonating. Avoids Escape — Chrome/macOS never delivers a keydown for Escape+modifier (why
2231
// the old Cmd+Esc did nothing). Matched on `event.code`, not `event.key`, because Option makes
@@ -34,7 +43,7 @@ export function GlobalShortcuts() {
3443

3544
document.addEventListener("keydown", onKeyDown);
3645
return () => document.removeEventListener("keydown", onKeyDown);
37-
}, [isAdmin, isImpersonating, navigate, submit]);
46+
}, [isAdmin, isImpersonating, navigate, submit, copyPublicUrl]);
3847

3948
return null;
4049
}

‎apps/webapp/app/components/Shortcuts.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useDashboardAgentAvailable } from "~/components/dashboard-agent/dashboa
66
import { NEW_CHAT_SHORTCUT } from "~/components/dashboard-agent/DashboardAgentHeader";
77
import { TOGGLE_PANEL_SHORTCUT } from "~/components/dashboard-agent/dashboardAgentLauncher";
88
import { COLUMNS_SHORTCUT } from "~/components/runs/v3/RunsDisplayOptions";
9+
import { useCopyPublicUrl } from "~/hooks/useCopyPublicUrl";
910
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
1011
import { Header3 } from "./primitives/Headers";
1112
import { SideMenuItemButton } from "./navigation/SideMenuItem";
@@ -48,6 +49,7 @@ export function ShortcutsAutoOpen() {
4849

4950
function ShortcutContent() {
5051
const agent = useDashboardAgentAvailable();
52+
const copyPublicUrl = useCopyPublicUrl();
5153
const rows = aiShortcutRows({ agent });
5254
const shows = (row: AiShortcutRow) => rows.includes(row);
5355

@@ -111,6 +113,12 @@ function ShortcutContent() {
111113
<Shortcut name="Help & Feedback">
112114
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
113115
</Shortcut>
116+
{copyPublicUrl.enabled && (
117+
<Shortcut name="Copy public URL">
118+
<ShortcutKey shortcut={{ modifiers: ["mod", "alt"] }} variant="medium/bright" />
119+
<ShortcutKey shortcut={{ key: "l" }} variant="medium/bright" />
120+
</Shortcut>
121+
)}
114122
</div>
115123
{shows("agent-new-chat") && (
116124
<div className="space-y-3">

‎apps/webapp/app/components/navigation/SideMenu.tsx‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { SlackIcon } from "~/assets/icons/SlackIcon";
5151
import { VercelLogo } from "~/components/integrations/VercelLogo";
5252
import { Avatar } from "~/components/primitives/Avatar";
5353
import { UserProfilePhoto } from "~/components/UserProfilePhoto";
54+
import { useCopyPublicUrl } from "~/hooks/useCopyPublicUrl";
5455
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
5556
import { usePageSwitcher } from "~/hooks/useEnvironmentSwitcher";
5657
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
@@ -1622,7 +1623,11 @@ function OrgSelector({
16221623
{isCollapsed && (
16231624
<div className="border-t border-grid-bright p-1">
16241625
<SideMenuPopoverSubMenu title="Account" icon={<UserProfilePhoto className="size-5" />}>
1625-
<AccountMenuItems isAdmin={isAdmin} isImpersonating={isImpersonating} />
1626+
<AccountMenuItems
1627+
isAdmin={isAdmin}
1628+
isImpersonating={isImpersonating}
1629+
onClose={() => setOrgMenuOpen(false)}
1630+
/>
16261631
</SideMenuPopoverSubMenu>
16271632
</div>
16281633
)}
@@ -1638,11 +1643,14 @@ function OrgSelector({
16381643
function AccountMenuItems({
16391644
isAdmin,
16401645
isImpersonating,
1646+
onClose,
16411647
}: {
16421648
isAdmin: boolean;
16431649
isImpersonating: boolean;
1650+
onClose: () => void;
16441651
}) {
16451652
const submit = useSubmit();
1653+
const copyPublicUrl = useCopyPublicUrl();
16461654
const stopImpersonating = () =>
16471655
submit(null, { action: "/resources/impersonation", method: "delete" });
16481656

@@ -1688,6 +1696,26 @@ function AccountMenuItems({
16881696
className={SIDE_MENU_POPOVER_ITEM_LABEL}
16891697
/>
16901698
)}
1699+
{copyPublicUrl.enabled && (
1700+
<PopoverMenuItem
1701+
title={
1702+
<div className="flex w-full items-center justify-between">
1703+
<span>Copy public URL</span>
1704+
<ShortcutKey
1705+
shortcut={{ modifiers: ["mod", "alt"], key: "l" }}
1706+
variant="medium/bright"
1707+
/>
1708+
</div>
1709+
}
1710+
icon={ChainLinkIcon}
1711+
onClick={() => {
1712+
copyPublicUrl.copy();
1713+
onClose();
1714+
}}
1715+
leadingIconClassName={SIDE_MENU_POPOVER_ITEM_ICON}
1716+
className={SIDE_MENU_POPOVER_ITEM_LABEL}
1717+
/>
1718+
)}
16911719
</div>
16921720
)}
16931721
<div className="flex flex-col gap-1 p-1">
@@ -1785,7 +1813,11 @@ function AccountMenu({ isAdmin, isImpersonating }: { isAdmin: boolean; isImperso
17851813
style={{ maxHeight: `calc(var(--radix-popover-content-available-height) - 10vh)` }}
17861814
>
17871815
<PopoverSectionHeader title="Account" />
1788-
<AccountMenuItems isAdmin={isAdmin} isImpersonating={isImpersonating} />
1816+
<AccountMenuItems
1817+
isAdmin={isAdmin}
1818+
isImpersonating={isImpersonating}
1819+
onClose={() => setIsOpen(false)}
1820+
/>
17891821
</PopoverContent>
17901822
</Popover>
17911823
);

‎apps/webapp/app/hooks/useAppOrigin.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ export function useAppOrigin() {
66

77
return routeMatch!.appOrigin;
88
}
9+
10+
export function usePublicAppOrigin(): string | undefined {
11+
const routeMatch = useTypedRouteLoaderData<typeof loader>("root");
12+
13+
return routeMatch!.publicAppOrigin;
14+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useLocation } from "@remix-run/react";
2+
import { useToast } from "~/components/primitives/Toast";
3+
import { useAppOrigin, usePublicAppOrigin } from "~/hooks/useAppOrigin";
4+
import { useHasAdminAccess } from "~/hooks/useUser";
5+
6+
export function useCopyPublicUrl(): { enabled: boolean; copy: () => void } {
7+
const { pathname, search, hash } = useLocation();
8+
const appOrigin = useAppOrigin();
9+
const publicAppOrigin = usePublicAppOrigin();
10+
const hasAdminAccess = useHasAdminAccess();
11+
const toast = useToast();
12+
13+
const isInternalPath = pathname.startsWith("/admin") || pathname.startsWith("/@");
14+
const enabled =
15+
hasAdminAccess &&
16+
publicAppOrigin !== undefined &&
17+
publicAppOrigin !== appOrigin &&
18+
!isInternalPath;
19+
20+
const copy = () => {
21+
if (!enabled) return;
22+
23+
const publicUrl = new URL(pathname + search + hash, publicAppOrigin).toString();
24+
navigator.clipboard.writeText(publicUrl).then(
25+
() => toast.success("Copied public URL"),
26+
() => toast.error("Couldn't copy public URL")
27+
);
28+
};
29+
30+
return { enabled, copy };
31+
}

‎apps/webapp/app/root.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
137137
features,
138138
appEnv: env.APP_ENV,
139139
appOrigin: env.APP_ORIGIN,
140+
publicAppOrigin: env.PUBLIC_APP_ORIGIN,
140141
apiOrigin: env.API_ORIGIN ?? env.APP_ORIGIN,
141142
dashboardAgentBaseUrl: env.DASHBOARD_AGENT_BASE_URL ?? "https://api.trigger.dev",
142143
triggerCliTag: env.TRIGGER_CLI_TAG,

0 commit comments

Comments
 (0)