Skip to content

Commit 21cac4c

Browse files
authored
Turn "View Markdown" button into a dropdown (#60130)
1 parent 8be6249 commit 21cac4c

File tree

8 files changed

+168
-28
lines changed

8 files changed

+168
-28
lines changed

data/ui.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@ pages:
9999
about_versions: About versions
100100
permissions_callout_title: Who can use this feature?
101101
video_from_transcript: See video for this transcript
102-
view_page_as_markdown: View page as Markdown
102+
copy_as_markdown: Copy as Markdown
103+
copy_as_markdown_desc: Use with any LLM
104+
view_as_markdown: View as Markdown
105+
view_as_markdown_desc: Open page in another tab
106+
ask_copilot: Ask Copilot
107+
ask_copilot_desc: Open directly in Copilot Chat
108+
more_markdown_options: More Markdown options
109+
opens_in_new_tab: (opens in new tab)
110+
copied: Copied!
103111
support:
104112
still_need_help: Still need help?
105113
contact_support: Contact support
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const ASK_AI_EVENT_GROUP = 'ask-ai'
2+
export const MARKDOWN_SOURCE_MENU_EVENT_GROUP = 'markdown-source-menu'
23
export const SEARCH_OVERLAY_EVENT_GROUP = 'search-overlay'
34
export const GENERAL_SEARCH_RESULTS = 'general-search-results'

src/events/lib/schema.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,7 @@ const link = {
362362
'toc',
363363
'footer',
364364
'static',
365-
'view-markdown-button',
366-
'page-source-menu',
365+
'markdown-source-menu',
367366
],
368367
description: 'The part of the page where the user clicked the link.',
369368
},

src/events/tests/middleware.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,26 @@ describe('POST /events', () => {
188188
expect(statusCode).toBe(200)
189189
}
190190
})
191+
192+
test('should accept a link event with markdown-source-menu container', async () => {
193+
const { statusCode } = await checkEvent({
194+
type: 'link',
195+
context: pageExample.context,
196+
link_url: 'https://docs.github.com/api/article/body?pathname=/en/copilot/overview',
197+
link_samesite: false,
198+
link_container: 'markdown-source-menu',
199+
})
200+
expect(statusCode).toBe(200)
201+
})
202+
203+
test('should reject a link event with an invalid link_container', async () => {
204+
const { statusCode } = await checkEvent({
205+
type: 'link',
206+
context: pageExample.context,
207+
link_url: 'https://docs.github.com/api/article/body?pathname=/en/copilot/overview',
208+
link_samesite: false,
209+
link_container: 'not-a-valid-container',
210+
})
211+
expect(statusCode).toBe(400)
212+
})
191213
})

src/fixtures/fixtures/data/ui.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@ pages:
9999
about_versions: About versions
100100
permissions_callout_title: Who can use this feature?
101101
video_from_transcript: See video for this transcript
102-
view_page_as_markdown: View page as Markdown
102+
copy_as_markdown: Copy as Markdown
103+
copy_as_markdown_desc: Use with any LLM
104+
view_as_markdown: View as Markdown
105+
view_as_markdown_desc: Open page in another tab
106+
ask_copilot: Ask Copilot
107+
ask_copilot_desc: Open directly in Copilot Chat
108+
more_markdown_options: More Markdown options
109+
opens_in_new_tab: (opens in new tab)
110+
copied: Copied!
103111
support:
104112
still_need_help: Still need help?
105113
contact_support: Contact support

src/frame/components/article/ArticlePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useTranslation } from '@/languages/components/useTranslation'
2323
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
2424
import { UtmPreserver } from '@/frame/components/UtmPreserver'
2525
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'
26-
import { ViewMarkdownButton } from './ViewMarkdownButton'
26+
import { CopyMarkdownMenu } from './ViewMarkdownButton'
2727
import { ExperimentContentSwap } from '@/events/components/experiments/ExperimentContentSwap'
2828

2929
const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), {
@@ -77,7 +77,7 @@ export const ArticlePage = () => {
7777

7878
const toc = (
7979
<>
80-
<ViewMarkdownButton currentPath={currentPath} />
80+
<CopyMarkdownMenu currentPath={currentPath} />
8181
{isLearningPath && <LearningTrackCard track={currentLearningTrack} />}
8282
{isJourneyTrack && <JourneyTrackCard journey={currentJourneyTrack} />}
8383
{miniTocItems.length > 1 && <MiniTocs miniTocItems={miniTocItems} />}
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
.button {
22
font-size: 12px;
33
padding: 4px 8px;
4-
border-color: #d1d9e0;
54
background-color: transparent;
6-
border-radius: 6px;
75
cursor: pointer;
86
}
Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,149 @@
1-
import { FileIcon } from '@primer/octicons-react'
2-
import { Button } from '@primer/react'
1+
import { useCallback } from 'react'
2+
import {
3+
CopyIcon,
4+
CopilotIcon,
5+
FileIcon,
6+
LinkExternalIcon,
7+
TriangleDownIcon,
8+
} from '@primer/octicons-react'
9+
import { ActionList, ActionMenu, Button, ButtonGroup, VisuallyHidden } from '@primer/react'
10+
import { announce } from '@primer/live-region-element'
11+
import { MARKDOWN_SOURCE_MENU_EVENT_GROUP } from '@/events/components/event-groups'
312
import { sendEvent } from '@/events/components/events'
413
import { EventType } from '@/events/types'
514
import { useTranslation } from '@/languages/components/useTranslation'
615
import cx from 'classnames'
716
import styles from './ViewMarkdownButton.module.scss'
817

9-
interface ViewMarkdownButtonProps {
18+
interface CopyMarkdownMenuProps {
1019
currentPath: string
1120
}
1221

13-
export const ViewMarkdownButton = ({ currentPath }: ViewMarkdownButtonProps) => {
22+
export const CopyMarkdownMenu = ({ currentPath }: CopyMarkdownMenuProps) => {
1423
const { t } = useTranslation('pages')
1524

1625
const encodedPath = encodeURIComponent(currentPath).replace(/%2F/g, '/').replace(/%40/g, '@')
1726
const markdownUrl = `/api/article/body?pathname=${encodedPath}`
1827

19-
const handleClick = () => {
28+
const docsUrl = `https://docs.github.com${encodedPath}`
29+
const copilotPrompt = `I need help with this GitHub Docs page: ${docsUrl}.md`
30+
const copilotUrl = `https://github.com/copilot?prompt=${encodeURIComponent(copilotPrompt)}`
31+
32+
const handleViewClick = useCallback(() => {
2033
sendEvent({
2134
type: EventType.link,
2235
link_url: `${window.location.origin}${markdownUrl}`,
2336
link_samesite: false,
24-
link_container: 'view-markdown-button',
37+
link_container: 'markdown-source-menu',
38+
eventGroupKey: MARKDOWN_SOURCE_MENU_EVENT_GROUP,
39+
})
40+
}, [markdownUrl])
41+
42+
const handleCopilotClick = useCallback(() => {
43+
sendEvent({
44+
type: EventType.link,
45+
link_url: copilotUrl,
46+
link_samesite: false,
47+
link_container: 'markdown-source-menu',
48+
eventGroupKey: MARKDOWN_SOURCE_MENU_EVENT_GROUP,
2549
})
26-
window.open(markdownUrl, '_blank')
27-
}
50+
}, [copilotUrl])
51+
52+
const handleCopyClick = useCallback(async () => {
53+
sendEvent({
54+
type: EventType.clipboard,
55+
clipboard_operation: 'copy',
56+
clipboard_target: markdownUrl,
57+
eventGroupKey: MARKDOWN_SOURCE_MENU_EVENT_GROUP,
58+
})
59+
try {
60+
const res = await fetch(markdownUrl)
61+
if (!res.ok) {
62+
throw new Error(`Failed to fetch: ${res.status}`)
63+
}
64+
const text = await res.text()
65+
await navigator.clipboard.writeText(text)
66+
announce(t('copied'))
67+
} catch {
68+
// Fallback: open in new tab if fetch or clipboard fails
69+
window.open(markdownUrl, '_blank')
70+
}
71+
}, [markdownUrl, t])
2872

2973
return (
3074
<div className="mb-3 ml-3">
31-
<Button
32-
onClick={handleClick}
33-
variant="default"
34-
className={cx(
35-
'd-inline-flex flex-items-center border text-decoration-none color-fg-default',
36-
styles.button,
37-
)}
38-
aria-label={t('view_page_as_markdown')}
39-
>
40-
<FileIcon size={12} className="mr-1" aria-hidden="true" />
41-
{t('view_page_as_markdown')}
42-
</Button>
75+
<ButtonGroup>
76+
<Button
77+
variant="default"
78+
className={cx(
79+
'd-inline-flex flex-items-center text-decoration-none color-fg-default',
80+
styles.button,
81+
)}
82+
onClick={handleCopyClick}
83+
>
84+
<CopyIcon size={12} className="mr-1" aria-hidden="true" />
85+
{t('copy_as_markdown')}
86+
</Button>
87+
<ActionMenu>
88+
<ActionMenu.Button
89+
aria-label={t('more_markdown_options')}
90+
icon={TriangleDownIcon}
91+
className={styles.button}
92+
/>
93+
<ActionMenu.Overlay align="start">
94+
<ActionList>
95+
<ActionList.Item onSelect={handleCopyClick}>
96+
<ActionList.LeadingVisual>
97+
<CopyIcon size={16} />
98+
</ActionList.LeadingVisual>
99+
{t('copy_as_markdown')}
100+
<ActionList.Description variant="block">
101+
{t('copy_as_markdown_desc')}
102+
</ActionList.Description>
103+
</ActionList.Item>
104+
<ActionList.LinkItem
105+
href={markdownUrl}
106+
target="_blank"
107+
rel="noopener noreferrer"
108+
onClick={handleViewClick}
109+
>
110+
<ActionList.LeadingVisual>
111+
<FileIcon size={16} />
112+
</ActionList.LeadingVisual>
113+
{t('view_as_markdown')}
114+
<VisuallyHidden>{t('opens_in_new_tab')}</VisuallyHidden>
115+
<ActionList.Description variant="block">
116+
{t('view_as_markdown_desc')}
117+
</ActionList.Description>
118+
<ActionList.TrailingVisual>
119+
<LinkExternalIcon size={16} aria-hidden="true" />
120+
</ActionList.TrailingVisual>
121+
</ActionList.LinkItem>
122+
<ActionList.LinkItem
123+
href={copilotUrl}
124+
target="_blank"
125+
rel="noopener noreferrer"
126+
onClick={handleCopilotClick}
127+
>
128+
<ActionList.LeadingVisual>
129+
<CopilotIcon size={16} />
130+
</ActionList.LeadingVisual>
131+
{t('ask_copilot')}
132+
<VisuallyHidden>{t('opens_in_new_tab')}</VisuallyHidden>
133+
<ActionList.Description variant="block">
134+
{t('ask_copilot_desc')}
135+
</ActionList.Description>
136+
<ActionList.TrailingVisual>
137+
<LinkExternalIcon size={16} aria-hidden="true" />
138+
</ActionList.TrailingVisual>
139+
</ActionList.LinkItem>
140+
</ActionList>
141+
</ActionMenu.Overlay>
142+
</ActionMenu>
143+
</ButtonGroup>
43144
</div>
44145
)
45146
}
147+
148+
/** @deprecated Use CopyMarkdownMenu instead */
149+
export const ViewMarkdownButton = CopyMarkdownMenu

0 commit comments

Comments
 (0)