Skip to content

Commit

Permalink
feat(ui): Add copy link + copy markdown dropdown to short ID (#53825)
Browse files Browse the repository at this point in the history
  • Loading branch information
evanpurkhiser authored Jul 31, 2023
1 parent de45caa commit 4ebc61d
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 37 deletions.
41 changes: 4 additions & 37 deletions static/app/views/issueDetails/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import styled from '@emotion/styled';
import {LocationDescriptor} from 'history';
import omit from 'lodash/omit';

import GuideAnchor from 'sentry/components/assistant/guideAnchor';
import Badge from 'sentry/components/badge';
import Breadcrumbs from 'sentry/components/breadcrumbs';
import Count from 'sentry/components/count';
Expand All @@ -14,14 +13,11 @@ import EventMessage from 'sentry/components/events/eventMessage';
import InboxReason from 'sentry/components/group/inboxBadges/inboxReason';
import {GroupStatusBadge} from 'sentry/components/group/inboxBadges/statusBadge';
import UnhandledInboxTag from 'sentry/components/group/inboxBadges/unhandledTag';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import * as Layout from 'sentry/components/layouts/thirds';
import Link from 'sentry/components/links/link';
import ReplayCountBadge from 'sentry/components/replays/replayCountBadge';
import useReplaysCount from 'sentry/components/replays/useReplaysCount';
import ShortId from 'sentry/components/shortId';
import {TabList} from 'sentry/components/tabs';
import {Tooltip} from 'sentry/components/tooltip';
import {IconChat} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
Expand All @@ -33,6 +29,7 @@ import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';

import GroupActions from './actions';
import {ShortIdBreadrcumb} from './shortIdBreadcrumb';
import {Tab} from './types';
import {TagAndMessageWrapper} from './unhandledTag';
import {ReprocessingStatus} from './utils';
Expand Down Expand Up @@ -227,26 +224,8 @@ function GroupHeader({

const disableActions = !!disabledTabs.length;

const shortIdBreadCrumb = group.shortId && (
<GuideAnchor target="issue_number" position="bottom">
<ShortIdBreadrcumb>
<ProjectBadge
project={project}
avatarSize={16}
hideName
avatarProps={{hasTooltip: true, tooltip: project.slug}}
/>
<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"
>
<StyledShortId shortId={group.shortId} />
</Tooltip>
</ShortIdBreadrcumb>
</GuideAnchor>
const shortIdBreadcrumb = (
<ShortIdBreadrcumb organization={organization} project={project} group={group} />
);

return (
Expand All @@ -259,7 +238,7 @@ function GroupHeader({
label: 'Issues',
to: `/organizations/${organization.slug}/issues/${location.search}`,
},
{label: shortIdBreadCrumb},
{label: shortIdBreadcrumb},
]}
/>
<GroupActions
Expand Down Expand Up @@ -335,18 +314,6 @@ const BreadcrumbActionWrapper = styled('div')`
align-items: center;
`;

const ShortIdBreadrcumb = 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 HeaderRow = styled('div')`
display: flex;
gap: ${space(2)};
Expand Down
46 changes: 46 additions & 0 deletions static/app/views/issueDetails/shortIdBreadcrumb.spec.tsx
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/)'
);
});
});
130 changes: 130 additions & 0 deletions static/app/views/issueDetails/shortIdBreadcrumb.tsx
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;
}
`;

0 comments on commit 4ebc61d

Please sign in to comment.