-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): Add copy link + copy markdown dropdown to short ID (#53825)
Looks like This ![image](https://github.com/getsentry/sentry/assets/1421724/7c251007-6e4c-4e5c-a7ca-3586b0c92073) When not hovered there's nothing ![clipboard.png](https://i.imgur.com/8nzVGD4.png)
- Loading branch information
1 parent
de45caa
commit 4ebc61d
Showing
3 changed files
with
180 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import {initializeOrg} from 'sentry-test/initializeOrg'; | ||
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; | ||
|
||
import {ShortIdBreadrcumb} from './shortIdBreadcrumb'; | ||
|
||
describe('ShortIdBreadrcumb', function () { | ||
const {organization, project} = initializeOrg(); | ||
const group = TestStubs.Group({shortId: 'ABC-123'}); | ||
|
||
beforeEach(() => { | ||
Object.assign(navigator, { | ||
clipboard: {writeText: jest.fn().mockResolvedValue('')}, | ||
}); | ||
}); | ||
|
||
it('renders short ID', function () { | ||
render(<ShortIdBreadrcumb {...{organization, project, group}} />); | ||
|
||
expect(screen.getByText('ABC-123')).toBeInTheDocument(); | ||
}); | ||
|
||
it('supports copy', async function () { | ||
render(<ShortIdBreadrcumb {...{organization, project, group}} />); | ||
|
||
async function clickMenuItem(name: string) { | ||
await userEvent.click(screen.getByRole('button', {name: 'Short-ID copy actions'})); | ||
await userEvent.click(screen.getByRole('menuitemradio', {name})); | ||
} | ||
|
||
// Copy short ID | ||
await clickMenuItem('Copy Short-ID'); | ||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('ABC-123'); | ||
|
||
// Copy short ID URL | ||
await clickMenuItem('Copy Issue URL'); | ||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith( | ||
'http://localhost/organizations/org-slug/issues/1/' | ||
); | ||
|
||
// Copy short ID Markdown | ||
await clickMenuItem('Copy Markdown Link'); | ||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith( | ||
'[ABC-123](http://localhost/organizations/org-slug/issues/1/)' | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import styled from '@emotion/styled'; | ||
|
||
import GuideAnchor from 'sentry/components/assistant/guideAnchor'; | ||
import {DropdownMenu} from 'sentry/components/dropdownMenu'; | ||
import ProjectBadge from 'sentry/components/idBadge/projectBadge'; | ||
import ShortId from 'sentry/components/shortId'; | ||
import {Tooltip} from 'sentry/components/tooltip'; | ||
import {IconChevron} from 'sentry/icons'; | ||
import {t} from 'sentry/locale'; | ||
import {space} from 'sentry/styles/space'; | ||
import {Group, Organization, Project} from 'sentry/types'; | ||
import useCopyToClipboard from 'sentry/utils/useCopyToClipboard'; | ||
import {normalizeUrl} from 'sentry/utils/withDomainRequired'; | ||
|
||
interface ShortIdBreadrcumbProps { | ||
group: Group; | ||
organization: Organization; | ||
project: Project; | ||
} | ||
|
||
export function ShortIdBreadrcumb({ | ||
organization, | ||
project, | ||
group, | ||
}: ShortIdBreadrcumbProps) { | ||
const {onClick: handleCopyShortId} = useCopyToClipboard({ | ||
text: group.shortId, | ||
successMessage: t('Copied Short-ID to clipboard'), | ||
}); | ||
|
||
const issueUrl = | ||
window.location.origin + | ||
normalizeUrl(`/organizations/${organization.slug}/issues/${group.id}/`); | ||
|
||
const {onClick: handleCopyUrl} = useCopyToClipboard({ | ||
text: issueUrl, | ||
successMessage: t('Copied Issue URL to clipboard'), | ||
}); | ||
|
||
const {onClick: handleCopyMarkdown} = useCopyToClipboard({ | ||
text: `[${group.shortId}](${issueUrl})`, | ||
successMessage: t('Copied Markdown Issue Link to clipboard'), | ||
}); | ||
|
||
if (!group.shortId) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<GuideAnchor target="issue_number" position="bottom"> | ||
<Wrapper> | ||
<ProjectBadge | ||
project={project} | ||
avatarSize={16} | ||
hideName | ||
avatarProps={{hasTooltip: true, tooltip: project.slug}} | ||
/> | ||
<ShortIdCopyable> | ||
<Tooltip | ||
className="help-link" | ||
title={t( | ||
'This identifier is unique across your organization, and can be used to reference an issue in various places, like commit messages.' | ||
)} | ||
position="bottom" | ||
delay={1000} | ||
> | ||
<StyledShortId shortId={group.shortId} /> | ||
</Tooltip> | ||
<DropdownMenu | ||
triggerProps={{ | ||
'aria-label': t('Short-ID copy actions'), | ||
icon: <IconChevron direction="down" size="xs" />, | ||
size: 'zero', | ||
borderless: true, | ||
showChevron: false, | ||
}} | ||
position="bottom" | ||
size="xs" | ||
items={[ | ||
{ | ||
key: 'copy-url', | ||
label: t('Copy Issue URL'), | ||
onAction: handleCopyUrl, | ||
}, | ||
{ | ||
key: 'copy-short-id', | ||
label: t('Copy Short-ID'), | ||
onAction: handleCopyShortId, | ||
}, | ||
{ | ||
key: 'copy-markdown-link', | ||
label: t('Copy Markdown Link'), | ||
onAction: handleCopyMarkdown, | ||
}, | ||
]} | ||
/> | ||
</ShortIdCopyable> | ||
</Wrapper> | ||
</GuideAnchor> | ||
); | ||
} | ||
|
||
const Wrapper = styled('div')` | ||
display: flex; | ||
gap: ${space(1)}; | ||
align-items: center; | ||
`; | ||
|
||
const StyledShortId = styled(ShortId)` | ||
font-family: ${p => p.theme.text.family}; | ||
font-size: ${p => p.theme.fontSizeMedium}; | ||
line-height: 1; | ||
`; | ||
|
||
const ShortIdCopyable = styled('div')` | ||
display: flex; | ||
gap: ${space(0.25)}; | ||
align-items: center; | ||
button[aria-haspopup] { | ||
opacity: 0; | ||
transition: opacity 50ms linear; | ||
} | ||
&:hover button[aria-haspopup], | ||
button[aria-expanded='true'], | ||
button[aria-haspopup].focus-visible { | ||
opacity: 1; | ||
} | ||
`; |