Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Add copy link + copy markdown dropdown to short ID #53825

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This menu is causing some sizing problems that I need to look into before merging this.

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;
}
`;